Merge lp:~blake-rouse/maas/node-list-columns into lp:~maas-committers/maas/trunk
- node-list-columns
- Merge into trunk
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 |
Related bugs: |
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.
Blake Rouse (blake-rouse) wrote : | # |
Added comment, fixing others.
Gavin Panella (allenap) : | # |
MAAS Lander (maas-lander) wrote : | # |
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://
Hit http://
Ign http://
Hit http://
Ign http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Hit http://
Ign http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Ign http://
Ign http://
Reading package lists...
sudo DEBIAN_
--
Preview Diff
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): |
Looks good, but some attention is needed before landing.