Merge lp:~rvb/maas/event-display into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 2646
Proposed branch: lp:~rvb/maas/event-display
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 367 lines (+221/-1)
9 files modified
src/maasserver/models/eventtype.py (+16/-0)
src/maasserver/models/tests/test_eventtype.py (+15/-0)
src/maasserver/static/css/components/table_list.css (+8/-0)
src/maasserver/templates/maasserver/node_event_list.html (+15/-0)
src/maasserver/templates/maasserver/node_event_list_snippet.html (+35/-0)
src/maasserver/templates/maasserver/node_view.html (+7/-0)
src/maasserver/urls.py (+4/-0)
src/maasserver/views/nodes.py (+29/-0)
src/maasserver/views/tests/test_nodes.py (+92/-1)
To merge this branch: bzr merge lp:~rvb/maas/event-display
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+229178@code.launchpad.net

Commit message

Add node event log UI: the latest 5 events are displayed on the node view page along with a link to see the full node event log.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good just a couple of things.

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

Thanks for the review!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/eventtype.py'
2--- src/maasserver/models/eventtype.py 2014-07-21 08:06:28 +0000
3+++ src/maasserver/models/eventtype.py 2014-08-01 13:36:30 +0000
4@@ -17,6 +17,8 @@
5 ]
6
7
8+import logging
9+
10 from django.db.models import (
11 CharField,
12 IntegerField,
13@@ -25,6 +27,15 @@
14 from maasserver.models.cleansave import CleanSave
15 from maasserver.models.timestampedmodel import TimestampedModel
16
17+# Describes how the log levels are displayed in the UI.
18+LOGGING_LEVELS = {
19+ logging.DEBUG: 'DEBUG',
20+ logging.INFO: 'INFO',
21+ logging.WARNING: 'WARNING',
22+ logging.ERROR: 'ERROR',
23+ logging.CRITICAL: 'CRITICAL',
24+}
25+
26
27 class EventType(CleanSave, TimestampedModel):
28 """A type for events.
29@@ -42,6 +53,11 @@
30
31 level = IntegerField(blank=False, editable=False)
32
33+ @property
34+ def level_str(self):
35+ """A human-readable version of the log level."""
36+ return LOGGING_LEVELS[self.level]
37+
38 class Meta(DefaultMeta):
39 verbose_name = "Event type"
40
41
42=== modified file 'src/maasserver/models/tests/test_eventtype.py'
43--- src/maasserver/models/tests/test_eventtype.py 2014-07-21 09:47:06 +0000
44+++ src/maasserver/models/tests/test_eventtype.py 2014-08-01 13:36:30 +0000
45@@ -14,6 +14,7 @@
46 __metaclass__ = type
47 __all__ = []
48
49+from maasserver.models.eventtype import LOGGING_LEVELS
50 from maasserver.testing.factory import factory
51 from maasserver.testing.testcase import MAASServerTestCase
52
53@@ -23,3 +24,17 @@
54 def test_displays_event_type_description(self):
55 event_type = factory.make_event_type()
56 self.assertIn(event_type.description, "%s" % event_type)
57+
58+ def test_level_str_returns_level_description(self):
59+ events_and_levels = [
60+ (
61+ level,
62+ factory.make_event(type=factory.make_event_type(level=level))
63+ )
64+ for level in LOGGING_LEVELS
65+ ]
66+
67+ self.assertEquals(
68+ [event.type.level_str for level, event in events_and_levels],
69+ [LOGGING_LEVELS[level] for level, event in events_and_levels],
70+ )
71
72=== modified file 'src/maasserver/static/css/components/table_list.css'
73--- src/maasserver/static/css/components/table_list.css 2014-02-25 07:27:05 +0000
74+++ src/maasserver/static/css/components/table_list.css 2014-08-01 13:36:30 +0000
75@@ -43,6 +43,14 @@
76 }
77
78 table.list tr.warning {
79+ background-color: #FEEFB3;
80+ }
81+
82+table.list tr.error, table.list tr.critical {
83+ background-color: #FFBABA;
84+ }
85+
86+table.list tr.critical {
87 background-color: #ecd7a9;
88 }
89
90
91=== added file 'src/maasserver/templates/maasserver/node_event_list.html'
92--- src/maasserver/templates/maasserver/node_event_list.html 1970-01-01 00:00:00 +0000
93+++ src/maasserver/templates/maasserver/node_event_list.html 2014-08-01 13:36:30 +0000
94@@ -0,0 +1,15 @@
95+{% extends "maasserver/base.html" %}
96+
97+{% block nav-active-event-list %}active{% endblock %}
98+{% block title %}Events{% endblock %}
99+{% block page-title %}
100+ {{ paginator.count }} event{{ paginator.count|pluralize }}
101+ for node {{ node.hostname }}
102+{% endblock %}
103+
104+{% block html_includes %}{% include "maasserver/snippets.html" %}
105+{% endblock %}
106+
107+{% block content %}
108+ {% include "maasserver/node_event_list_snippet.html" %}
109+{% endblock %}
110
111=== added file 'src/maasserver/templates/maasserver/node_event_list_snippet.html'
112--- src/maasserver/templates/maasserver/node_event_list_snippet.html 1970-01-01 00:00:00 +0000
113+++ src/maasserver/templates/maasserver/node_event_list_snippet.html 2014-08-01 13:36:30 +0000
114@@ -0,0 +1,35 @@
115+<div id="node_event_list">
116+ {% if event_list %}
117+ <table class="list">
118+ <thead>
119+ <tr>
120+ <th>Level</th>
121+ <th>Timestamp</th>
122+ <th>Event</th>
123+ </tr>
124+ </thead>
125+ <tbody>
126+ {% for event_item in event_list %}
127+ <tr {% if event_item.type.level_str == 'WARNING' %} class="warning"
128+ {% elif event_item.type.level_str == 'ERROR' %} class="error"
129+ {% elif event_item.type.level_str == 'CRITICAL' %} class="critical"
130+ {% endif %} id="{{ event_item.id }}">
131+ <td width="10%">{{ event_item.type.level_str }}</td>
132+ <td width="30%">{{ event_item.created|date:'Y-m-d H:i:s' }}</td>
133+ <td class="event_description">
134+ {{ event_item.type.description }}
135+ {% if event_item.description %}
136+ / {{ event_item.description }}
137+ {% endif %}
138+ </td>
139+ <td></td>
140+ </tr>
141+ {% endfor %}
142+ </tbody>
143+ </table>
144+ {% include "maasserver/pagination.html" %}
145+ {% else %}
146+ No event.
147+ {% endif %}
148+ <div class="clear"></div>
149+</div>
150
151=== modified file 'src/maasserver/templates/maasserver/node_view.html'
152--- src/maasserver/templates/maasserver/node_view.html 2014-04-15 12:51:56 +0000
153+++ src/maasserver/templates/maasserver/node_view.html 2014-08-01 13:36:30 +0000
154@@ -222,6 +222,13 @@
155 </span>
156 </li>
157 {% endif %}
158+ <li class="block first separate">
159+ <h2>Latest node events</h2>
160+ {% include "maasserver/node_event_list_snippet.html" %}
161+ {% if event_count != 0 %}
162+ <a href="{% url 'node-event-list-view' object.system_id %}">Full node event log ({{ event_count }} events).</a>
163+ {% endif %}
164+ </li>
165 {% if probed_details %}
166 <li class="block first separate">
167 <h2>Raw discovery data</h2>
168
169=== modified file 'src/maasserver/urls.py'
170--- src/maasserver/urls.py 2014-07-16 01:12:00 +0000
171+++ src/maasserver/urls.py 2014-08-01 13:36:30 +0000
172@@ -54,6 +54,7 @@
173 MacDelete,
174 NodeDelete,
175 NodeEdit,
176+ NodeEventListView,
177 NodeListView,
178 NodePreseedView,
179 NodeView,
180@@ -143,6 +144,9 @@
181 url(r'^nodes/enlist-preseed/$', enlist_preseed_view,
182 name='enlist-preseed-view'),
183 url(
184+ r'^nodes/(?P<system_id>[\w\-]+)/events/$', NodeEventListView.as_view(),
185+ name='node-event-list-view'),
186+ url(
187 r'^nodes/(?P<system_id>[\w\-]+)/view/$', NodeView.as_view(),
188 name='node-view'),
189 url(
190
191=== modified file 'src/maasserver/views/nodes.py'
192--- src/maasserver/views/nodes.py 2014-07-31 16:00:15 +0000
193+++ src/maasserver/views/nodes.py 2014-08-01 13:36:30 +0000
194@@ -17,6 +17,7 @@
195 'MacAdd',
196 'MacDelete',
197 'NodeDelete',
198+ 'NodeEventListView',
199 'NodeListView',
200 'NodePreseedView',
201 'NodeView',
202@@ -74,6 +75,7 @@
203 Tag,
204 )
205 from maasserver.models.config import Config
206+from maasserver.models.event import Event
207 from maasserver.models.nodeprobeddetails import get_single_probed_details
208 from maasserver.node_action import ACTIONS_DICT
209 from maasserver.node_constraint_filter_forms import (
210@@ -522,6 +524,9 @@
211 def get_form_class(self):
212 return get_action_form(self.request.user, self.request)
213
214+ # The number of events shown on the node view page.
215+ number_of_events_shown = 5
216+
217 def get_context_data(self, **kwargs):
218 context = super(NodeView, self).get_context_data(**kwargs)
219 node = self.get_object()
220@@ -561,6 +566,13 @@
221 'enable_third_party_drivers')
222 context['drivers'] = get_third_party_driver(node)
223
224+ event_list = (
225+ Event.objects.filter(node=self.get_object())
226+ .order_by('-id')[:self.number_of_events_shown])
227+ context['event_list'] = event_list
228+ context['event_count'] = Event.objects.filter(
229+ node=self.get_object()).count()
230+
231 return context
232
233 def dispatch(self, *args, **kwargs):
234@@ -579,6 +591,23 @@
235 return reverse('node-view', args=[self.get_object().system_id])
236
237
238+class NodeEventListView(NodeViewMixin, PaginatedListView):
239+
240+ context_object_name = "event_list"
241+
242+ template_name = "maasserver/node_event_list.html"
243+
244+ def get_queryset(self):
245+ return Event.objects.filter(
246+ node=self.get_object()).order_by('-id')
247+
248+ def get_context_data(self, **kwargs):
249+ context = super(NodeEventListView, self).get_context_data(**kwargs)
250+ node = self.get_object()
251+ context['node'] = node
252+ return context
253+
254+
255 class NodeEdit(UpdateView):
256
257 template_name = 'maasserver/node_edit.html'
258
259=== modified file 'src/maasserver/views/tests/test_nodes.py'
260--- src/maasserver/views/tests/test_nodes.py 2014-07-31 10:56:18 +0000
261+++ src/maasserver/views/tests/test_nodes.py 2014-08-01 13:36:30 +0000
262@@ -66,7 +66,11 @@
263 from maasserver.third_party_drivers import get_third_party_driver
264 from maasserver.utils.orm import get_one
265 from maasserver.views import nodes as nodes_views
266-from maasserver.views.nodes import message_from_form_stats
267+from maasserver.views.nodes import (
268+ message_from_form_stats,
269+ NodeEventListView,
270+ NodeView,
271+ )
272 from maastesting.djangotestcase import count_queries
273 from metadataserver.models.commissioningscript import (
274 LIST_MODALIASES_OUTPUT_NAME,
275@@ -1149,6 +1153,93 @@
276 response = self.client.get(reverse('node-view', args=[node.system_id]))
277 self.assertNotIn("Third Party Drivers", response.content)
278
279+ def test_node_view_show_latest_node_events(self):
280+ self.client_log_in()
281+ node = factory.make_node()
282+ # Create old events.
283+ [
284+ factory.make_event(node=node)
285+ for _ in range(4)
286+ ]
287+ # Create NodeView.number_of_events_shown events.
288+ events = [
289+ factory.make_event(node=node)
290+ for _ in range(NodeView.number_of_events_shown)
291+ ]
292+ response = self.client.get(reverse('node-view', args=[node.system_id]))
293+ self.assertIn("Latest node events", response.content)
294+ document = fromstring(response.content)
295+ events_displayed = document.xpath(
296+ "//div[@id='node_event_list']//td[@class='event_description']")
297+ self.assertItemsEqual(
298+ [event.type.description for event in events],
299+ [display.text_content().strip() for display in events_displayed]
300+ )
301+
302+ def test_node_view_doesnt_show_events_from_other_nodes(self):
303+ self.client_log_in()
304+ node = factory.make_node()
305+ # Create an event related to another node.
306+ event = factory.make_event()
307+ response = self.client.get(
308+ reverse('node-view', args=[node.system_id]))
309+ self.assertIn("Latest node events", response.content)
310+ document = fromstring(response.content)
311+ events_displayed = document.xpath("//div[@id='node_event_list']")
312+ self.assertNotIn(
313+ event.type.description,
314+ events_displayed[0].text_content().strip(),
315+ )
316+
317+ def test_node_view_contains_link_to_node_event_log(self):
318+ self.client_log_in()
319+ node = factory.make_node()
320+ # Create an event related to another node.
321+ [
322+ factory.make_event(node=node)
323+ for _ in range(4)
324+ ]
325+ response = self.client.get(
326+ reverse('node-view', args=[node.system_id]))
327+ node_event_list = reverse(
328+ 'node-event-list-view', args=[node.system_id])
329+ self.assertIn(node_event_list, get_content_links(response))
330+
331+
332+class NodeEventLogTest(MAASServerTestCase):
333+
334+ def test_event_log_shows_event_list(self):
335+ self.client_log_in()
336+ node = factory.make_node()
337+ events = [
338+ factory.make_event(node=node)
339+ for _ in range(NodeView.number_of_events_shown)
340+ ]
341+ response = self.client.get(
342+ reverse('node-event-list-view', args=[node.system_id]))
343+ document = fromstring(response.content)
344+ events_displayed = document.xpath(
345+ "//div[@id='node_event_list']//td[@class='event_description']")
346+ self.assertItemsEqual(
347+ [event.type.description for event in events],
348+ [display.text_content().strip() for display in events_displayed]
349+ )
350+
351+ def test_event_log_is_paginated(self):
352+ self.client_log_in()
353+ self.patch(NodeEventListView, "paginate_by", 3)
354+ node = factory.make_node()
355+ # Create 4 events.
356+ [factory.make_event(node=node) for _ in range(4)]
357+
358+ response = self.client.get(
359+ reverse('node-event-list-view', args=[node.system_id]))
360+ self.assertEqual(httplib.OK, response.status_code)
361+ doc = fromstring(response.content)
362+ self.assertEqual(
363+ 1, len(doc.cssselect('div.pagination')),
364+ "Couldn't find pagination tag.")
365+
366
367 class ConstructThirdPartyDriversNoticeTest(MAASServerTestCase):
368