Merge lp:~julian-edwards/maas/better-search into lp:~maas-committers/maas/trunk

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 3328
Proposed branch: lp:~julian-edwards/maas/better-search
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 448 lines (+225/-31)
4 files modified
src/maasserver/static/css/components/search_box.css (+51/-9)
src/maasserver/templates/maasserver/node_list.html (+13/-6)
src/maasserver/views/nodes.py (+24/-3)
src/maasserver/views/tests/test_nodes.py (+137/-13)
To merge this branch: bzr merge lp:~julian-edwards/maas/better-search
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Christian Reis (community) Needs Information
Review via email: mp+239793@code.launchpad.net

Commit message

Add an extra search box to the node listing page that allows users to do substring searches that match on hostname, architecture and tags.

Description of the change

I suck at CSS and it shows, so please help me out here! There's a couple of obvious problems with the new layout of the boxes (please run up the branch to see).

 1. The magnifying glass icon moves up a pixel when you hover over it! But only on one of the search boxes... :/

 2. The cross-to-cancel click doesn't work on the "test constraints" box, I think I messed up with the layout somehow but I don't know how.

Otherwise, this does exactly what it says on the tin.

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

Pretty please someone?

Revision history for this message
Raphaël Badin (rvb) wrote :

The new search box is really badly positioned on Firefox (it overlaps with the other search box): http://people.canonical.com/~rvb/search_overlap.png

Revision history for this message
Raphaël Badin (rvb) wrote :

This is the result of searching for 'u' on the sample data: http://people.canonical.com/~rvb/multiple_nodes.png. Some lines are duplicated!

review: Needs Fixing
Revision history for this message
Raphaël Badin (rvb) wrote :

Not sure if it's a regression this branch introduced or not but the sorting parameters are not preserved when you use the search box:
- sort on a field
- search for something
- the ordering is lost

Revision history for this message
Christian Reis (kiko) wrote :

Is this a part of the UX work that is being planned post-1.7?

review: Needs Information
Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 30 Oct 2014 11:50:31 you wrote:
> Review: Needs Information
>
> Is this a part of the UX work that is being planned post-1.7?

No, it was a bug that Andres wanted fixed.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 30 Oct 2014 11:10:40 you wrote:
> Review: Needs Fixing
>
> This is the result of searching for 'u' on the sample data:
> http://people.canonical.com/~rvb/multiple_nodes.png. Some lines are
> duplicated!

Woops, needs a unique! Thanks for spotting.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 30 Oct 2014 10:49:42 you wrote:
> The new search box is really badly positioned on Firefox (it overlaps with
> the other search box): http://people.canonical.com/~rvb/search_overlap.png

I hate CSS. I mean, I really hate it. :/

Revision history for this message
Christian Reis (kiko) wrote :

> No, it was a bug that Andres wanted fixed.

Okay, then we shouldn't do this without aligning with the wider UX plan.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 30 Oct 2014 13:25:30 you wrote:
> > No, it was a bug that Andres wanted fixed.
>
> Okay, then we shouldn't do this without aligning with the wider UX plan.

I really don't think that should block this, we have had people screaming for
a proper search for a long time. The aesthetics are orthogonal to the
underlying search and can be trivially tweaked later - by someone who knows
what they're doing with CSS of course :)

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 30 October 2014 11:10:40 you wrote:
> Review: Needs Fixing
>
> This is the result of searching for 'u' on the sample data:
> http://people.canonical.com/~rvb/multiple_nodes.png. Some lines are
> duplicated!

FWIW, I can't get the sampledata to work when I load it from fresh, it looks
like it needs fixing, it causes the appserver to crash when clicking on nodes,
and the names in the node list are all "#### INVALID STRING ####"

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Also regarding sorting being preserved, it's like that in trunk AFAICT.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Ok Raph, I've fixed the duplication, but I'll be buggered if I can get the CSS working. When I change it to fix one browser, the other screws up. I think I need some help here, if you are able?

Revision history for this message
Christian Reis (kiko) wrote :

Julian, have you looked at Blake's search work which he shared via the list? Looks like the two of you should sync.

Revision history for this message
Raphaël Badin (rvb) wrote :

> Woops, needs a unique! Thanks for spotting.

It needs a unique indeed. But it has nothing to do with the union:
This is a way to trigger the problem: http://paste.ubuntu.com/8803036/ (fwiw, this is the query this generates http://paste.ubuntu.com/8803039/).

The problem is that querying across manytomany relationships gives you an effective join. Hence the duplication. This makes perfect sense from an SQL point of view but from an ORM point of view (i.e. Nodes.objects.filter()), it's a bit weird, I'll give you that.

Also, you can make the code is a tad nicer: http://paste.ubuntu.com/8803384/

Revision history for this message
Raphaël Badin (rvb) wrote :

> When I change it to fix one browser, the other screws up. I think I need some help here, if you are able?

I don't see the overlap I was seeing last week, I guess you've fixed this.

The only UI problem that I see is the jumping magnifying glass… and the only way I managed to fix the problem is by doing this: http://paste.ubuntu.com/8804644/.

Revision history for this message
Raphaël Badin (rvb) wrote :

This looks good to me. This is a massive improvement to the usability of MAAS. I'm not entirely sure we should have the two search boxes though. It's looks weird and I'm not sure anyone uses the constraints search box since it isn't documented anywhere.

So I'd be +1 on removing it after we make sure its usage is non-existant. But this is something we can do after this lands.

>> Okay, then we shouldn't do this without aligning with the wider UX plan.

> [...] The aesthetics are orthogonal to the underlying search.

Agreed. I even think it's best if we land this now to force the UX redesign to take this into account.

review: Approve
Revision history for this message
Christian Reis (kiko) wrote :

On Mon, Nov 03, 2014 at 04:06:07PM -0000, Raphaël Badin wrote:
> Agreed. I even think it's best if we land this now to force the UX
> redesign to take this into account.

Won't this directly conflict with what Blake is working on?

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Monday 03 November 2014 12:50:53 you wrote:
> Julian, have you looked at Blake's search work which he shared via the list?
> Looks like the two of you should sync.

I did. My change is vastly simpler and I'd like to land this now because it
solves a specific problem without requiring a new JS toolkit and UI redesign.

It doesn't preclude any work that Blake is doing, however.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Monday 03 November 2014 18:18:21 you wrote:
> On Mon, Nov 03, 2014 at 04:06:07PM -0000, Raphaël Badin wrote:
> > Agreed. I even think it's best if we land this now to force the UX
> > redesign to take this into account.
>
> Won't this directly conflict with what Blake is working on?

Not really, his is nowhere near landing.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Monday 03 November 2014 16:00:34 you wrote:
> > When I change it to fix one browser, the other screws up. I think I need
> > some help here, if you are able?
> I don't see the overlap I was seeing last week, I guess you've fixed this.

I did. Copious overriding of CSS properties!

> The only UI problem that I see is the jumping magnifying glass… and the only
> way I managed to fix the problem is by doing this:

> http://paste.ubuntu.com/8804644/.

I worked out what's causing it in the end, I can just remove the padding in
the input that it's inheriting from some top-level CSS class. The hover makes
the padding move! Weird.

Also the other problem is that the input boxes are different lengths on FF and
Chrom*. FF increases the size of the box when you left-pad the text to make
room for the icon; Chrom* does not. Consistency eh?!

I need to fix one more thing too, the margins are different sizes on each box
(again, css inheritance...).

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Monday 03 November 2014 16:06:07 you wrote:
> Review: Approve

Thanks.

> This looks good to me. This is a massive improvement to the usability of
> MAAS. I'm not entirely sure we should have the two search boxes though.
> It's looks weird and I'm not sure anyone uses the constraints search box
> since it isn't documented anywhere.

I'm not sure either, but I left it *just in case*.

> So I'd be +1 on removing it after we make sure its usage is non-existant.
> But this is something we can do after this lands.

Yeah, let's ask around.

> >> Okay, then we shouldn't do this without aligning with the wider UX plan.
> >
> > [...] The aesthetics are orthogonal to the underlying search.
>
> Agreed. I even think it's best if we land this now to force the UX redesign
> to take this into account.

+1

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Bouncing icon fixed - the CSS was being overridden in the HTML page but not for the hover attribute... fun!

Now, I've sorted all of that and the FF page looks great. Chrom* looks good, except the shorter input boxes and that the cursor is now off centre in the box until you start typing in it!

You just can't f'in win.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/static/css/components/search_box.css'
--- src/maasserver/static/css/components/search_box.css 2014-02-25 07:27:05 +0000
+++ src/maasserver/static/css/components/search_box.css 2014-11-04 01:15:28 +0000
@@ -2,21 +2,42 @@
2 position: absolute;2 position: absolute;
3 top: 0;3 top: 0;
4 right: 0;4 right: 0;
5 width: 210px;
6 height: 33px;
7 }
8
9form.search-test {
10 position: absolute;
11 top: 0;
12 right: 220px;
5 width: 420px;13 width: 420px;
6 height: 26px;14 height: 33px;
7 }15 }
816
9#header form.search {17#header form.search {
10 position: relative;18 position: relative;
11 width: 228px;19 width: 420px;
12 margin: 18px 15px 0 0;20 margin: 18px 15px 0 0;
13 }21 }
1422
23input.search-test {
24 position: relative;
25 top: 0;
26 left: -20px;
27 width: 420px;
28 height: 33px;
29 padding: 0;
30 padding-left: 22px;
31 background-color: #fff;
32 }
33
15input.search-input {34input.search-input {
16 position: absolute;35 position: relative;
17 top: 0;36 top: 0;
18 left: 0;37 left: 0px;
19 width: 420px;38 width: 186px;
39 height: 33px;
40 padding: 0;
20 padding-left: 22px;41 padding-left: 22px;
21 background-color: #fff;42 background-color: #fff;
22 }43 }
@@ -29,10 +50,34 @@
29 border-radius: 4px;50 border-radius: 4px;
30 }51 }
3152
53#header input.search-test-input {
54 width: 225px;
55 border: none;
56 -moz-border-radius: 4px;
57 -webkit-border-radius: 4px;
58 border-radius: 4px;
59 }
60
61input.search-test-submit,
62input.search-test-submit:hover {
63 position: absolute;
64 top: 4px;
65 margin-top: 3px;
66 left: -19px;
67 width: 24px;
68 height: 24px;
69 padding: 0;
70 margin: 0;
71 background: transparent url(../?img/search_icon.png) no-repeat center center;
72 border: none;
73 font-size: 0;
74 cursor: pointer;
75 }
76
32input.search-submit,77input.search-submit,
33input.search-submit:hover {78input.search-submit:hover {
34 position: absolute;79 position: absolute;
35 top: 1px;80 top: 4px;
36 left: 1px;81 left: 1px;
37 width: 24px;82 width: 24px;
38 height: 24px;83 height: 24px;
@@ -41,8 +86,5 @@
41 background: transparent url(../?img/search_icon.png) no-repeat center center;86 background: transparent url(../?img/search_icon.png) no-repeat center center;
42 border: none;87 border: none;
43 font-size: 0;88 font-size: 0;
44 }
45
46input.search-submit:hover {
47 cursor: pointer;89 cursor: pointer;
48 }90 }
4991
=== modified file 'src/maasserver/templates/maasserver/node_list.html'
--- src/maasserver/templates/maasserver/node_list.html 2014-10-08 08:37:42 +0000
+++ src/maasserver/templates/maasserver/node_list.html 2014-11-04 01:15:28 +0000
@@ -2,7 +2,7 @@
22
3{% block nav-active-node-list %}active{% endblock %}3{% block nav-active-node-list %}active{% endblock %}
4{% block title %}Nodes{% endblock %}4{% block title %}Nodes{% endblock %}
5{% block page-title %}{{ paginator.count }}{% if input_query %} matching{% endif %} node{{ paginator.count|pluralize }} in {% include "maasserver/site_title.html" %}{% endblock %}5{% block page-title %}{{ paginator.count }}{% if input_query or input_test %} matching{% endif %} node{{ paginator.count|pluralize }} in {% include "maasserver/site_title.html" %}{% endblock %}
6{% block header-search %}{% endblock %}6{% block header-search %}{% endblock %}
77
8{% block html_includes %}{% include "maasserver/snippets.html" %}8{% block html_includes %}{% include "maasserver/snippets.html" %}
@@ -27,14 +27,13 @@
27 display:none;27 display:none;
28 }28 }
2929
30 input.search-input {30 input.search-input,
31 input.search-test {
31 line-height: 23px;32 line-height: 23px;
32 outline: none;33 outline: none;
33 }34 }
3435
35 input.search-submit {36
36 margin-top: 3px;
37 }
38 </style>37 </style>
39 <script type="text/javascript">38 <script type="text/javascript">
40 <!--39 <!--
@@ -101,12 +100,20 @@
101100
102{% block content %}101{% block content %}
103 <div id="nodes" style="position: relative;">102 <div id="nodes" style="position: relative;">
103 <form id="node_listing_constraint_form"
104 action="{% url 'node-list' %}?{{preserved_query}}"
105 method="get" class="search-test">
106 <input type="search" name="test" placeholder="Test Juju constraints"
107 class="search-test" value="{{input_test|default_if_none:''}}" />
108 <input id="search_button" type="submit"
109 class="search-test-submit" />
110 </form>
104 <form id="node_listing_search_form"111 <form id="node_listing_search_form"
105 action="{% url 'node-list' %}?{{preserved_query}}"112 action="{% url 'node-list' %}?{{preserved_query}}"
106 method="get" class="search">113 method="get" class="search">
107 <input type="search" name="query" placeholder="Search nodes"114 <input type="search" name="query" placeholder="Search nodes"
108 class="search-input" value="{{input_query|default_if_none:''}}" />115 class="search-input" value="{{input_query|default_if_none:''}}" />
109 <input id="search_button" type="submit" value="Search"116 <input id="search_button" type="submit"
110 class="search-submit" />117 class="search-submit" />
111 </form>118 </form>
112119
113120
=== modified file 'src/maasserver/views/nodes.py'
--- src/maasserver/views/nodes.py 2014-10-31 07:11:08 +0000
+++ src/maasserver/views/nodes.py 2014-11-04 01:15:28 +0000
@@ -35,6 +35,7 @@
35from django.contrib import messages35from django.contrib import messages
36from django.core.exceptions import PermissionDenied36from django.core.exceptions import PermissionDenied
37from django.core.urlresolvers import reverse37from django.core.urlresolvers import reverse
38from django.db.models import Q
38from django.http import QueryDict39from django.http import QueryDict
39from django.shortcuts import (40from django.shortcuts import (
40 get_object_or_404,41 get_object_or_404,
@@ -228,6 +229,7 @@
228229
229 def populate_modifiers(self, request):230 def populate_modifiers(self, request):
230 self.query = request.GET.get("query")231 self.query = request.GET.get("query")
232 self.test_query = request.GET.get("test")
231 self.query_error = None233 self.query_error = None
232 self.sort_by = request.GET.get("sort")234 self.sort_by = request.GET.get("sort")
233 self.sort_dir = request.GET.get("dir")235 self.sort_dir = request.GET.get("dir")
@@ -253,7 +255,7 @@
253 These are sorting and search option we want a POST request to255 These are sorting and search option we want a POST request to
254 preserve so that the display after a POST request is similar256 preserve so that the display after a POST request is similar
255 to the display before the request."""257 to the display before the request."""
256 return ["dir", "query", "page", "sort"]258 return ["dir", "query", "test", "page", "sort"]
257259
258 def get_preserved_query(self):260 def get_preserved_query(self):
259 params = {261 params = {
@@ -323,7 +325,7 @@
323 :param nodes_query: A query set of nodes.325 :param nodes_query: A query set of nodes.
324 :return: A query set of nodes that returns a subset of `nodes_query`.326 :return: A query set of nodes that returns a subset of `nodes_query`.
325 """327 """
326 data = _parse_constraints(self.query)328 data = _parse_constraints(self.test_query)
327 form = AcquireNodeForm.Strict(data=data)329 form = AcquireNodeForm.Strict(data=data)
328 # Change the field names of the AcquireNodeForm object to330 # Change the field names of the AcquireNodeForm object to
329 # conform to Juju's naming.331 # conform to Juju's naming.
@@ -336,12 +338,30 @@
336 for field, errors in form.errors.items()])338 for field, errors in form.errors.items()])
337 return Node.objects.none()339 return Node.objects.none()
338340
341 def _search_nodes(self, nodes_query):
342 """Filter the given nodes query by searching field data.
343
344 The search query is substring matched non-case sensitively
345 against some fields in the nodes.
346
347 :param nodes_query: A queryset of nodes.
348 :return: A query set of nodes that returns a subset of `nodes_query`.
349 """
350 name_clause = Q(hostname__icontains=self.query)
351 arch_clause = Q(architecture__icontains=self.query)
352 tag_clause = Q(tags__name__icontains=self.query)
353
354 query = nodes_query.filter(name_clause | arch_clause | tag_clause)
355 return query.distinct()
356
339 def get_queryset(self):357 def get_queryset(self):
340 nodes = Node.objects.get_nodes(358 nodes = Node.objects.get_nodes(
341 user=self.request.user, perm=NODE_PERMISSION.VIEW)359 user=self.request.user, perm=NODE_PERMISSION.VIEW)
342 nodes = nodes.order_by(*self._compose_sort_order())360 nodes = nodes.order_by(*self._compose_sort_order())
361 if self.test_query:
362 nodes = self._constrain_nodes(nodes)
343 if self.query:363 if self.query:
344 nodes = self._constrain_nodes(nodes)364 nodes = self._search_nodes(nodes)
345 nodes = prefetch_nodes_listing(nodes)365 nodes = prefetch_nodes_listing(nodes)
346 return configure_macs(nodes)366 return configure_macs(nodes)
347367
@@ -391,6 +411,7 @@
391 context["preserved_query"] = self.get_preserved_query()411 context["preserved_query"] = self.get_preserved_query()
392 context["form"] = form412 context["form"] = form
393 context["input_query"] = self.query413 context["input_query"] = self.query
414 context["input_test"] = self.test_query
394 context["input_query_error"] = self.query_error415 context["input_query_error"] = self.query_error
395 links, classes = self._prepare_sort_links()416 links, classes = self._prepare_sort_links()
396 context["sort_links"] = links417 context["sort_links"] = links
397418
=== modified file 'src/maasserver/views/tests/test_nodes.py'
--- src/maasserver/views/tests/test_nodes.py 2014-10-31 07:11:08 +0000
+++ src/maasserver/views/tests/test_nodes.py 2014-11-04 01:15:28 +0000
@@ -94,7 +94,9 @@
94from provisioningserver.utils.enum import map_enum94from provisioningserver.utils.enum import map_enum
95from provisioningserver.utils.text import normalise_whitespace95from provisioningserver.utils.text import normalise_whitespace
96from testtools.matchers import (96from testtools.matchers import (
97 Contains,
97 ContainsAll,98 ContainsAll,
99 Equals,
98 HasLength,100 HasLength,
99 Not,101 Not,
100 )102 )
@@ -1029,31 +1031,31 @@
1029 "This node is now allocated to you.",1031 "This node is now allocated to you.",
1030 '\n'.join(msg.message for msg in response.context['messages']))1032 '\n'.join(msg.message for msg in response.context['messages']))
10311033
1032 def test_node_list_query_includes_current(self):1034 def test_node_list_constraint_test_query_includes_current(self):
1033 self.client_log_in()1035 self.client_log_in()
1034 qs = factory.make_string()1036 qs = factory.make_string()
1035 response = self.client.get(reverse('node-list'), {"query": qs})1037 response = self.client.get(reverse('node-list'), {"test": qs})
1036 query_value = fromstring(response.content).xpath(1038 query_value = fromstring(response.content).xpath(
1037 "string(//div[@id='nodes']//input[@name='query']/@value)")1039 "string(//div[@id='nodes']//input[@name='test']/@value)")
1038 self.assertIn(qs, query_value)1040 self.assertIn(qs, query_value)
10391041
1040 def test_node_list_query_error_on_missing_tag(self):1042 def test_node_list_constraint_test_query_error_on_missing_tag(self):
1041 self.client_log_in()1043 self.client_log_in()
1042 response = self.client.get(1044 response = self.client.get(
1043 reverse('node-list'), {"query": "maas-tags=missing"})1045 reverse('node-list'), {"test": "maas-tags=missing"})
1044 error_string = fromstring(response.content).xpath(1046 error_string = fromstring(response.content).xpath(
1045 "string(//div[@id='nodes']//p[@class='form-errors'])")1047 "string(//div[@id='nodes']//p[@class='form-errors'])")
1046 self.assertIn("No such tag(s): 'missing'", error_string)1048 self.assertIn("No such tag(s): 'missing'", error_string)
10471049
1048 def test_node_list_query_error_on_unknown_constraint(self):1050 def test_node_list_constraint_test_query_error_on_unknown_constraint(self):
1049 self.client_log_in()1051 self.client_log_in()
1050 response = self.client.get(1052 response = self.client.get(
1051 reverse('node-list'), {"query": "color=red"})1053 reverse('node-list'), {"test": "color=red"})
1052 error_string = fromstring(response.content).xpath(1054 error_string = fromstring(response.content).xpath(
1053 "string(//div[@id='nodes']//p[@class='form-errors'])")1055 "string(//div[@id='nodes']//p[@class='form-errors'])")
1054 self.assertEqual("color: No such constraint.", error_string.strip())1056 self.assertEqual("color: No such constraint.", error_string.strip())
10551057
1056 def test_node_list_query_selects_subset(self):1058 def test_node_list_constraint_test_query_selects_subset(self):
1057 self.client_log_in()1059 self.client_log_in()
1058 tag = factory.make_Tag("shiny")1060 tag = factory.make_Tag("shiny")
1059 node1 = factory.make_Node(cpu_count=1)1061 node1 = factory.make_Node(cpu_count=1)
@@ -1063,7 +1065,7 @@
1063 node2.tags = [tag]1065 node2.tags = [tag]
1064 node3.tags = []1066 node3.tags = []
1065 response = self.client.get(1067 response = self.client.get(
1066 reverse('node-list'), {"query": "maas-tags=shiny cpu=2"})1068 reverse('node-list'), {"test": "maas-tags=shiny cpu=2"})
1067 node2_link = reverse('node-view', args=[node2.system_id])1069 node2_link = reverse('node-view', args=[node2.system_id])
1068 document = fromstring(response.content)1070 document = fromstring(response.content)
1069 node_links = document.xpath(1071 node_links = document.xpath(
@@ -1116,7 +1118,7 @@
1116 [(a.text.lower(), a.get("href"))1118 [(a.text.lower(), a.get("href"))
1117 for a in expr_page_anchors(page3)])1119 for a in expr_page_anchors(page3)])
11181120
1119 def test_node_list_query_paginates(self):1121 def test_node_list_constraint_test_query_paginates(self):
1120 """Node list query subset is split across multiple pages with links"""1122 """Node list query subset is split across multiple pages with links"""
1121 self.client_log_in()1123 self.client_log_in()
1122 # Set a very small page size to save creating lots of nodes1124 # Set a very small page size to save creating lots of nodes
@@ -1130,7 +1132,7 @@
1130 last_node_link = reverse('node-view', args=[nodes[0].system_id])1132 last_node_link = reverse('node-view', args=[nodes[0].system_id])
1131 response = self.client.get(1133 response = self.client.get(
1132 reverse('node-list'),1134 reverse('node-list'),
1133 {"query": "maas-tags=odd", "page": 3})1135 {"test": "maas-tags=odd", "page": 3})
1134 document = fromstring(response.content)1136 document = fromstring(response.content)
1135 self.assertIn("5 matching nodes", document.xpath("string(//h1)"))1137 self.assertIn("5 matching nodes", document.xpath("string(//h1)"))
1136 self.assertEqual(1138 self.assertEqual(
@@ -1138,8 +1140,8 @@
1138 document.xpath("//div[@id='nodes']/form/table/tr/td[3]/a/@href"))1140 document.xpath("//div[@id='nodes']/form/table/tr/td[3]/a/@href"))
1139 self.assertEqual(1141 self.assertEqual(
1140 [1142 [
1141 ("first", "?query=maas-tags%3Dodd"),1143 ("first", "?test=maas-tags%3Dodd"),
1142 ("previous", "?query=maas-tags%3Dodd&page=2")1144 ("previous", "?test=maas-tags%3Dodd&page=2")
1143 ],1145 ],
1144 [1146 [
1145 (a.text.lower(), a.get("href"))1147 (a.text.lower(), a.get("href"))
@@ -1369,6 +1371,128 @@
1369 self.assertIn(node_event_list, get_content_links(response))1371 self.assertIn(node_event_list, get_content_links(response))
13701372
13711373
1374class TestNodesViewSearch(MAASServerTestCase):
1375
1376 def test_node_list_search_query_includes_current_clause(self):
1377 self.client_log_in()
1378 qs = factory.make_string()
1379 response = self.client.get(reverse('node-list'), {"query": qs})
1380 query_value = fromstring(response.content).xpath(
1381 "string(//div[@id='nodes']//input[@name='query']/@value)")
1382 self.assertIn(qs, query_value)
1383
1384 def test_node_list_search_query_finds_by_hostname(self):
1385 self.client_log_in()
1386 [factory.make_Node() for i in range(10)]
1387 hostname = factory.make_name("hostname")
1388 node = factory.make_Node(hostname=hostname)
1389 node2 = factory.make_Node(hostname=hostname[:-1])
1390 node_link = reverse('node-view', args=[node.system_id])
1391 node2_link = reverse('node-view', args=[node2.system_id])
1392 response = self.client.get(reverse('node-list'), {"query": hostname})
1393 document = fromstring(response.content)
1394 node_links = document.xpath(
1395 "//div[@id='nodes']/form/table/tr/td[3]/a/@href")
1396 self.expectThat(node_links, Equals([node_link]))
1397
1398 # A substring search also matches. Here it also picks up the
1399 # other node with the exact name match.
1400 response = self.client.get(
1401 reverse('node-list'), {"query": hostname[:-1]})
1402 document = fromstring(response.content)
1403 node_links = document.xpath(
1404 "//div[@id='nodes']/form/table/tr/td[3]/a/@href")
1405 self.expectThat(node_links, Equals([node_link, node2_link]))
1406
1407 def test_node_list_search_query_finds_by_arch(self):
1408 self.client_log_in()
1409 amd64_node = factory.make_Node(architecture="amd64/generic")
1410 factory.make_Node(architecture="i386/generic")
1411 factory.make_Node(architecture="armhf/generic")
1412
1413 node_link = reverse('node-view', args=[amd64_node.system_id])
1414 response = self.client.get(reverse('node-list'), {"query": "amd64"})
1415 document = fromstring(response.content)
1416 node_links = document.xpath(
1417 "//div[@id='nodes']/form/table/tr/td[3]/a/@href")
1418 self.expectThat(node_links, Equals([node_link]))
1419
1420 def test_node_list_search_doesnt_show_duplicates(self):
1421 # Unioning several query sets can result in dupes, check that
1422 # the query uses distinct.
1423 self.client_log_in()
1424 node = factory.make_Node(
1425 hostname="arthur", architecture="amd64/generic")
1426 node.tags = [factory.make_Tag() for i in range(3)]
1427 node.save()
1428 nodes = [node]
1429 nodes.append(factory.make_Node(
1430 hostname="andrew", architecture="i386/generic"))
1431 response = self.client.get(reverse('node-list'), {"query": "a"})
1432 document = fromstring(response.content)
1433 expected_node_links = [
1434 reverse('node-view', args=[n.system_id]) for n in nodes]
1435 doc_node_links = document.xpath(
1436 "//div[@id='nodes']/form/table/tr/td[3]/a/@href")
1437 self.assertItemsEqual(expected_node_links, doc_node_links)
1438
1439 def test_node_list_search_query_finds_by_tags(self):
1440 self.client_log_in()
1441 nodes = [factory.make_Node() for i in range(10)]
1442 tag = factory.make_Tag("odd")
1443 for node in nodes[::2]:
1444 node.tags = [tag]
1445 response = self.client.get(
1446 reverse('node-list'), {"query": "odd"})
1447 document = fromstring(response.content)
1448 node_links = document.xpath(
1449 "//div[@id='nodes']/form/table/tr/td[3]/a/@href")
1450 expected_node_links = [
1451 reverse('node-view', args=[node.system_id])
1452 for node in nodes[::2]]
1453 self.expectThat(node_links, ContainsAll(expected_node_links))
1454
1455 # Substrings match tags too.
1456 response = self.client.get(
1457 reverse('node-list'), {"query": "od"})
1458 document = fromstring(response.content)
1459 node_links = document.xpath(
1460 "//div[@id='nodes']/form/table/tr/td[3]/a/@href")
1461 self.expectThat(node_links, ContainsAll(expected_node_links))
1462
1463 def test_node_list_search_query_paginates(self):
1464 """Node list query subset is split across multiple pages with links"""
1465 self.client_log_in()
1466 # Set a very small page size to save creating lots of nodes
1467 self.patch(nodes_views.NodeListView, 'paginate_by', 2)
1468 nodes = [
1469 factory.make_Node(created="2012-10-12 12:00:%02d" % i)
1470 for i in range(10)]
1471 tag = factory.make_Tag("odd")
1472 for node in nodes[::2]:
1473 node.tags = [tag]
1474 last_node_link = reverse('node-view', args=[nodes[0].system_id])
1475 response = self.client.get(
1476 reverse('node-list'), {"query": "odd", "page": 3})
1477 document = fromstring(response.content)
1478 self.expectThat(
1479 document.xpath("string(//h1)"), Contains("5 matching nodes"))
1480 self.expectThat(
1481 [last_node_link],
1482 Equals(
1483 document.xpath(
1484 "//div[@id='nodes']/form/table/tr/td[3]/a/@href")))
1485 self.assertEqual(
1486 [
1487 ("first", "?query=odd"),
1488 ("previous", "?query=odd&page=2")
1489 ],
1490 [
1491 (a.text.lower(), a.get("href"))
1492 for a in document.xpath("//div[@class='pagination']//a")
1493 ])
1494
1495
1372class TestWarnUnconfiguredIPAddresses(MAASServerTestCase):1496class TestWarnUnconfiguredIPAddresses(MAASServerTestCase):
13731497
1374 def test__warns_for_IPv6_address_on_non_ubuntu_OS(self):1498 def test__warns_for_IPv6_address_on_non_ubuntu_OS(self):