Merge lp:~julian-edwards/maas/better-search into lp:~maas-committers/maas/trunk
- better-search
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
Julian Edwards (julian-edwards) wrote : | # |
Raphaël Badin (rvb) wrote : | # |
The new search box is really badly positioned on Firefox (it overlaps with the other search box): http://
Raphaël Badin (rvb) wrote : | # |
This is the result of searching for 'u' on the sample data: http://
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
Christian Reis (kiko) wrote : | # |
Is this a part of the UX work that is being planned post-1.7?
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.
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://
> duplicated!
Woops, needs a unique! Thanks for spotting.
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://
I hate CSS. I mean, I really hate it. :/
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.
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 :)
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://
> 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 ####"
Julian Edwards (julian-edwards) wrote : | # |
Also regarding sorting being preserved, it's like that in trunk AFAICT.
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?
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.
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://
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.
Also, you can make the code is a tad nicer: http://
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://
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.
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?
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.
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.
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://
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...).
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
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
1 | === modified file 'src/maasserver/static/css/components/search_box.css' |
2 | --- src/maasserver/static/css/components/search_box.css 2014-02-25 07:27:05 +0000 |
3 | +++ src/maasserver/static/css/components/search_box.css 2014-11-04 01:15:28 +0000 |
4 | @@ -2,21 +2,42 @@ |
5 | position: absolute; |
6 | top: 0; |
7 | right: 0; |
8 | + width: 210px; |
9 | + height: 33px; |
10 | + } |
11 | + |
12 | +form.search-test { |
13 | + position: absolute; |
14 | + top: 0; |
15 | + right: 220px; |
16 | width: 420px; |
17 | - height: 26px; |
18 | + height: 33px; |
19 | } |
20 | |
21 | #header form.search { |
22 | position: relative; |
23 | - width: 228px; |
24 | + width: 420px; |
25 | margin: 18px 15px 0 0; |
26 | } |
27 | |
28 | +input.search-test { |
29 | + position: relative; |
30 | + top: 0; |
31 | + left: -20px; |
32 | + width: 420px; |
33 | + height: 33px; |
34 | + padding: 0; |
35 | + padding-left: 22px; |
36 | + background-color: #fff; |
37 | + } |
38 | + |
39 | input.search-input { |
40 | - position: absolute; |
41 | + position: relative; |
42 | top: 0; |
43 | - left: 0; |
44 | - width: 420px; |
45 | + left: 0px; |
46 | + width: 186px; |
47 | + height: 33px; |
48 | + padding: 0; |
49 | padding-left: 22px; |
50 | background-color: #fff; |
51 | } |
52 | @@ -29,10 +50,34 @@ |
53 | border-radius: 4px; |
54 | } |
55 | |
56 | +#header input.search-test-input { |
57 | + width: 225px; |
58 | + border: none; |
59 | + -moz-border-radius: 4px; |
60 | + -webkit-border-radius: 4px; |
61 | + border-radius: 4px; |
62 | + } |
63 | + |
64 | +input.search-test-submit, |
65 | +input.search-test-submit:hover { |
66 | + position: absolute; |
67 | + top: 4px; |
68 | + margin-top: 3px; |
69 | + left: -19px; |
70 | + width: 24px; |
71 | + height: 24px; |
72 | + padding: 0; |
73 | + margin: 0; |
74 | + background: transparent url(../?img/search_icon.png) no-repeat center center; |
75 | + border: none; |
76 | + font-size: 0; |
77 | + cursor: pointer; |
78 | + } |
79 | + |
80 | input.search-submit, |
81 | input.search-submit:hover { |
82 | position: absolute; |
83 | - top: 1px; |
84 | + top: 4px; |
85 | left: 1px; |
86 | width: 24px; |
87 | height: 24px; |
88 | @@ -41,8 +86,5 @@ |
89 | background: transparent url(../?img/search_icon.png) no-repeat center center; |
90 | border: none; |
91 | font-size: 0; |
92 | - } |
93 | - |
94 | -input.search-submit:hover { |
95 | cursor: pointer; |
96 | } |
97 | |
98 | === modified file 'src/maasserver/templates/maasserver/node_list.html' |
99 | --- src/maasserver/templates/maasserver/node_list.html 2014-10-08 08:37:42 +0000 |
100 | +++ src/maasserver/templates/maasserver/node_list.html 2014-11-04 01:15:28 +0000 |
101 | @@ -2,7 +2,7 @@ |
102 | |
103 | {% block nav-active-node-list %}active{% endblock %} |
104 | {% block title %}Nodes{% endblock %} |
105 | -{% block page-title %}{{ paginator.count }}{% if input_query %} matching{% endif %} node{{ paginator.count|pluralize }} in {% include "maasserver/site_title.html" %}{% endblock %} |
106 | +{% block page-title %}{{ paginator.count }}{% if input_query or input_test %} matching{% endif %} node{{ paginator.count|pluralize }} in {% include "maasserver/site_title.html" %}{% endblock %} |
107 | {% block header-search %}{% endblock %} |
108 | |
109 | {% block html_includes %}{% include "maasserver/snippets.html" %} |
110 | @@ -27,14 +27,13 @@ |
111 | display:none; |
112 | } |
113 | |
114 | - input.search-input { |
115 | + input.search-input, |
116 | + input.search-test { |
117 | line-height: 23px; |
118 | outline: none; |
119 | } |
120 | |
121 | - input.search-submit { |
122 | - margin-top: 3px; |
123 | - } |
124 | + |
125 | </style> |
126 | <script type="text/javascript"> |
127 | <!-- |
128 | @@ -101,12 +100,20 @@ |
129 | |
130 | {% block content %} |
131 | <div id="nodes" style="position: relative;"> |
132 | + <form id="node_listing_constraint_form" |
133 | + action="{% url 'node-list' %}?{{preserved_query}}" |
134 | + method="get" class="search-test"> |
135 | + <input type="search" name="test" placeholder="Test Juju constraints" |
136 | + class="search-test" value="{{input_test|default_if_none:''}}" /> |
137 | + <input id="search_button" type="submit" |
138 | + class="search-test-submit" /> |
139 | + </form> |
140 | <form id="node_listing_search_form" |
141 | action="{% url 'node-list' %}?{{preserved_query}}" |
142 | method="get" class="search"> |
143 | <input type="search" name="query" placeholder="Search nodes" |
144 | class="search-input" value="{{input_query|default_if_none:''}}" /> |
145 | - <input id="search_button" type="submit" value="Search" |
146 | + <input id="search_button" type="submit" |
147 | class="search-submit" /> |
148 | </form> |
149 | |
150 | |
151 | === modified file 'src/maasserver/views/nodes.py' |
152 | --- src/maasserver/views/nodes.py 2014-10-31 07:11:08 +0000 |
153 | +++ src/maasserver/views/nodes.py 2014-11-04 01:15:28 +0000 |
154 | @@ -35,6 +35,7 @@ |
155 | from django.contrib import messages |
156 | from django.core.exceptions import PermissionDenied |
157 | from django.core.urlresolvers import reverse |
158 | +from django.db.models import Q |
159 | from django.http import QueryDict |
160 | from django.shortcuts import ( |
161 | get_object_or_404, |
162 | @@ -228,6 +229,7 @@ |
163 | |
164 | def populate_modifiers(self, request): |
165 | self.query = request.GET.get("query") |
166 | + self.test_query = request.GET.get("test") |
167 | self.query_error = None |
168 | self.sort_by = request.GET.get("sort") |
169 | self.sort_dir = request.GET.get("dir") |
170 | @@ -253,7 +255,7 @@ |
171 | These are sorting and search option we want a POST request to |
172 | preserve so that the display after a POST request is similar |
173 | to the display before the request.""" |
174 | - return ["dir", "query", "page", "sort"] |
175 | + return ["dir", "query", "test", "page", "sort"] |
176 | |
177 | def get_preserved_query(self): |
178 | params = { |
179 | @@ -323,7 +325,7 @@ |
180 | :param nodes_query: A query set of nodes. |
181 | :return: A query set of nodes that returns a subset of `nodes_query`. |
182 | """ |
183 | - data = _parse_constraints(self.query) |
184 | + data = _parse_constraints(self.test_query) |
185 | form = AcquireNodeForm.Strict(data=data) |
186 | # Change the field names of the AcquireNodeForm object to |
187 | # conform to Juju's naming. |
188 | @@ -336,12 +338,30 @@ |
189 | for field, errors in form.errors.items()]) |
190 | return Node.objects.none() |
191 | |
192 | + def _search_nodes(self, nodes_query): |
193 | + """Filter the given nodes query by searching field data. |
194 | + |
195 | + The search query is substring matched non-case sensitively |
196 | + against some fields in the nodes. |
197 | + |
198 | + :param nodes_query: A queryset of nodes. |
199 | + :return: A query set of nodes that returns a subset of `nodes_query`. |
200 | + """ |
201 | + name_clause = Q(hostname__icontains=self.query) |
202 | + arch_clause = Q(architecture__icontains=self.query) |
203 | + tag_clause = Q(tags__name__icontains=self.query) |
204 | + |
205 | + query = nodes_query.filter(name_clause | arch_clause | tag_clause) |
206 | + return query.distinct() |
207 | + |
208 | def get_queryset(self): |
209 | nodes = Node.objects.get_nodes( |
210 | user=self.request.user, perm=NODE_PERMISSION.VIEW) |
211 | nodes = nodes.order_by(*self._compose_sort_order()) |
212 | + if self.test_query: |
213 | + nodes = self._constrain_nodes(nodes) |
214 | if self.query: |
215 | - nodes = self._constrain_nodes(nodes) |
216 | + nodes = self._search_nodes(nodes) |
217 | nodes = prefetch_nodes_listing(nodes) |
218 | return configure_macs(nodes) |
219 | |
220 | @@ -391,6 +411,7 @@ |
221 | context["preserved_query"] = self.get_preserved_query() |
222 | context["form"] = form |
223 | context["input_query"] = self.query |
224 | + context["input_test"] = self.test_query |
225 | context["input_query_error"] = self.query_error |
226 | links, classes = self._prepare_sort_links() |
227 | context["sort_links"] = links |
228 | |
229 | === modified file 'src/maasserver/views/tests/test_nodes.py' |
230 | --- src/maasserver/views/tests/test_nodes.py 2014-10-31 07:11:08 +0000 |
231 | +++ src/maasserver/views/tests/test_nodes.py 2014-11-04 01:15:28 +0000 |
232 | @@ -94,7 +94,9 @@ |
233 | from provisioningserver.utils.enum import map_enum |
234 | from provisioningserver.utils.text import normalise_whitespace |
235 | from testtools.matchers import ( |
236 | + Contains, |
237 | ContainsAll, |
238 | + Equals, |
239 | HasLength, |
240 | Not, |
241 | ) |
242 | @@ -1029,31 +1031,31 @@ |
243 | "This node is now allocated to you.", |
244 | '\n'.join(msg.message for msg in response.context['messages'])) |
245 | |
246 | - def test_node_list_query_includes_current(self): |
247 | + def test_node_list_constraint_test_query_includes_current(self): |
248 | self.client_log_in() |
249 | qs = factory.make_string() |
250 | - response = self.client.get(reverse('node-list'), {"query": qs}) |
251 | + response = self.client.get(reverse('node-list'), {"test": qs}) |
252 | query_value = fromstring(response.content).xpath( |
253 | - "string(//div[@id='nodes']//input[@name='query']/@value)") |
254 | + "string(//div[@id='nodes']//input[@name='test']/@value)") |
255 | self.assertIn(qs, query_value) |
256 | |
257 | - def test_node_list_query_error_on_missing_tag(self): |
258 | + def test_node_list_constraint_test_query_error_on_missing_tag(self): |
259 | self.client_log_in() |
260 | response = self.client.get( |
261 | - reverse('node-list'), {"query": "maas-tags=missing"}) |
262 | + reverse('node-list'), {"test": "maas-tags=missing"}) |
263 | error_string = fromstring(response.content).xpath( |
264 | "string(//div[@id='nodes']//p[@class='form-errors'])") |
265 | self.assertIn("No such tag(s): 'missing'", error_string) |
266 | |
267 | - def test_node_list_query_error_on_unknown_constraint(self): |
268 | + def test_node_list_constraint_test_query_error_on_unknown_constraint(self): |
269 | self.client_log_in() |
270 | response = self.client.get( |
271 | - reverse('node-list'), {"query": "color=red"}) |
272 | + reverse('node-list'), {"test": "color=red"}) |
273 | error_string = fromstring(response.content).xpath( |
274 | "string(//div[@id='nodes']//p[@class='form-errors'])") |
275 | self.assertEqual("color: No such constraint.", error_string.strip()) |
276 | |
277 | - def test_node_list_query_selects_subset(self): |
278 | + def test_node_list_constraint_test_query_selects_subset(self): |
279 | self.client_log_in() |
280 | tag = factory.make_Tag("shiny") |
281 | node1 = factory.make_Node(cpu_count=1) |
282 | @@ -1063,7 +1065,7 @@ |
283 | node2.tags = [tag] |
284 | node3.tags = [] |
285 | response = self.client.get( |
286 | - reverse('node-list'), {"query": "maas-tags=shiny cpu=2"}) |
287 | + reverse('node-list'), {"test": "maas-tags=shiny cpu=2"}) |
288 | node2_link = reverse('node-view', args=[node2.system_id]) |
289 | document = fromstring(response.content) |
290 | node_links = document.xpath( |
291 | @@ -1116,7 +1118,7 @@ |
292 | [(a.text.lower(), a.get("href")) |
293 | for a in expr_page_anchors(page3)]) |
294 | |
295 | - def test_node_list_query_paginates(self): |
296 | + def test_node_list_constraint_test_query_paginates(self): |
297 | """Node list query subset is split across multiple pages with links""" |
298 | self.client_log_in() |
299 | # Set a very small page size to save creating lots of nodes |
300 | @@ -1130,7 +1132,7 @@ |
301 | last_node_link = reverse('node-view', args=[nodes[0].system_id]) |
302 | response = self.client.get( |
303 | reverse('node-list'), |
304 | - {"query": "maas-tags=odd", "page": 3}) |
305 | + {"test": "maas-tags=odd", "page": 3}) |
306 | document = fromstring(response.content) |
307 | self.assertIn("5 matching nodes", document.xpath("string(//h1)")) |
308 | self.assertEqual( |
309 | @@ -1138,8 +1140,8 @@ |
310 | document.xpath("//div[@id='nodes']/form/table/tr/td[3]/a/@href")) |
311 | self.assertEqual( |
312 | [ |
313 | - ("first", "?query=maas-tags%3Dodd"), |
314 | - ("previous", "?query=maas-tags%3Dodd&page=2") |
315 | + ("first", "?test=maas-tags%3Dodd"), |
316 | + ("previous", "?test=maas-tags%3Dodd&page=2") |
317 | ], |
318 | [ |
319 | (a.text.lower(), a.get("href")) |
320 | @@ -1369,6 +1371,128 @@ |
321 | self.assertIn(node_event_list, get_content_links(response)) |
322 | |
323 | |
324 | +class TestNodesViewSearch(MAASServerTestCase): |
325 | + |
326 | + def test_node_list_search_query_includes_current_clause(self): |
327 | + self.client_log_in() |
328 | + qs = factory.make_string() |
329 | + response = self.client.get(reverse('node-list'), {"query": qs}) |
330 | + query_value = fromstring(response.content).xpath( |
331 | + "string(//div[@id='nodes']//input[@name='query']/@value)") |
332 | + self.assertIn(qs, query_value) |
333 | + |
334 | + def test_node_list_search_query_finds_by_hostname(self): |
335 | + self.client_log_in() |
336 | + [factory.make_Node() for i in range(10)] |
337 | + hostname = factory.make_name("hostname") |
338 | + node = factory.make_Node(hostname=hostname) |
339 | + node2 = factory.make_Node(hostname=hostname[:-1]) |
340 | + node_link = reverse('node-view', args=[node.system_id]) |
341 | + node2_link = reverse('node-view', args=[node2.system_id]) |
342 | + response = self.client.get(reverse('node-list'), {"query": hostname}) |
343 | + document = fromstring(response.content) |
344 | + node_links = document.xpath( |
345 | + "//div[@id='nodes']/form/table/tr/td[3]/a/@href") |
346 | + self.expectThat(node_links, Equals([node_link])) |
347 | + |
348 | + # A substring search also matches. Here it also picks up the |
349 | + # other node with the exact name match. |
350 | + response = self.client.get( |
351 | + reverse('node-list'), {"query": hostname[:-1]}) |
352 | + document = fromstring(response.content) |
353 | + node_links = document.xpath( |
354 | + "//div[@id='nodes']/form/table/tr/td[3]/a/@href") |
355 | + self.expectThat(node_links, Equals([node_link, node2_link])) |
356 | + |
357 | + def test_node_list_search_query_finds_by_arch(self): |
358 | + self.client_log_in() |
359 | + amd64_node = factory.make_Node(architecture="amd64/generic") |
360 | + factory.make_Node(architecture="i386/generic") |
361 | + factory.make_Node(architecture="armhf/generic") |
362 | + |
363 | + node_link = reverse('node-view', args=[amd64_node.system_id]) |
364 | + response = self.client.get(reverse('node-list'), {"query": "amd64"}) |
365 | + document = fromstring(response.content) |
366 | + node_links = document.xpath( |
367 | + "//div[@id='nodes']/form/table/tr/td[3]/a/@href") |
368 | + self.expectThat(node_links, Equals([node_link])) |
369 | + |
370 | + def test_node_list_search_doesnt_show_duplicates(self): |
371 | + # Unioning several query sets can result in dupes, check that |
372 | + # the query uses distinct. |
373 | + self.client_log_in() |
374 | + node = factory.make_Node( |
375 | + hostname="arthur", architecture="amd64/generic") |
376 | + node.tags = [factory.make_Tag() for i in range(3)] |
377 | + node.save() |
378 | + nodes = [node] |
379 | + nodes.append(factory.make_Node( |
380 | + hostname="andrew", architecture="i386/generic")) |
381 | + response = self.client.get(reverse('node-list'), {"query": "a"}) |
382 | + document = fromstring(response.content) |
383 | + expected_node_links = [ |
384 | + reverse('node-view', args=[n.system_id]) for n in nodes] |
385 | + doc_node_links = document.xpath( |
386 | + "//div[@id='nodes']/form/table/tr/td[3]/a/@href") |
387 | + self.assertItemsEqual(expected_node_links, doc_node_links) |
388 | + |
389 | + def test_node_list_search_query_finds_by_tags(self): |
390 | + self.client_log_in() |
391 | + nodes = [factory.make_Node() for i in range(10)] |
392 | + tag = factory.make_Tag("odd") |
393 | + for node in nodes[::2]: |
394 | + node.tags = [tag] |
395 | + response = self.client.get( |
396 | + reverse('node-list'), {"query": "odd"}) |
397 | + document = fromstring(response.content) |
398 | + node_links = document.xpath( |
399 | + "//div[@id='nodes']/form/table/tr/td[3]/a/@href") |
400 | + expected_node_links = [ |
401 | + reverse('node-view', args=[node.system_id]) |
402 | + for node in nodes[::2]] |
403 | + self.expectThat(node_links, ContainsAll(expected_node_links)) |
404 | + |
405 | + # Substrings match tags too. |
406 | + response = self.client.get( |
407 | + reverse('node-list'), {"query": "od"}) |
408 | + document = fromstring(response.content) |
409 | + node_links = document.xpath( |
410 | + "//div[@id='nodes']/form/table/tr/td[3]/a/@href") |
411 | + self.expectThat(node_links, ContainsAll(expected_node_links)) |
412 | + |
413 | + def test_node_list_search_query_paginates(self): |
414 | + """Node list query subset is split across multiple pages with links""" |
415 | + self.client_log_in() |
416 | + # Set a very small page size to save creating lots of nodes |
417 | + self.patch(nodes_views.NodeListView, 'paginate_by', 2) |
418 | + nodes = [ |
419 | + factory.make_Node(created="2012-10-12 12:00:%02d" % i) |
420 | + for i in range(10)] |
421 | + tag = factory.make_Tag("odd") |
422 | + for node in nodes[::2]: |
423 | + node.tags = [tag] |
424 | + last_node_link = reverse('node-view', args=[nodes[0].system_id]) |
425 | + response = self.client.get( |
426 | + reverse('node-list'), {"query": "odd", "page": 3}) |
427 | + document = fromstring(response.content) |
428 | + self.expectThat( |
429 | + document.xpath("string(//h1)"), Contains("5 matching nodes")) |
430 | + self.expectThat( |
431 | + [last_node_link], |
432 | + Equals( |
433 | + document.xpath( |
434 | + "//div[@id='nodes']/form/table/tr/td[3]/a/@href"))) |
435 | + self.assertEqual( |
436 | + [ |
437 | + ("first", "?query=odd"), |
438 | + ("previous", "?query=odd&page=2") |
439 | + ], |
440 | + [ |
441 | + (a.text.lower(), a.get("href")) |
442 | + for a in document.xpath("//div[@class='pagination']//a") |
443 | + ]) |
444 | + |
445 | + |
446 | class TestWarnUnconfiguredIPAddresses(MAASServerTestCase): |
447 | |
448 | def test__warns_for_IPv6_address_on_non_ubuntu_OS(self): |
Pretty please someone?