Merge lp:~allenap/maas/notifications-creation into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5702
Proposed branch: lp:~allenap/maas/notifications-creation
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 544 lines (+100/-133)
19 files modified
src/maasserver/bootsources.py (+3/-1)
src/maasserver/components.py (+30/-20)
src/maasserver/context_processors.py (+0/-2)
src/maasserver/migrations/builtin/maasserver/0111_remove_component_error.py (+14/-0)
src/maasserver/models/__init__.py (+0/-2)
src/maasserver/models/component_error.py (+0/-30)
src/maasserver/models/notification.py (+9/-2)
src/maasserver/models/tests/test_notification.py (+7/-0)
src/maasserver/static/js/angular/directives/notifications.js (+1/-1)
src/maasserver/static/js/angular/directives/tests/test_notifications.js (+13/-0)
src/maasserver/static/js/angular/maas.js (+1/-1)
src/maasserver/templates/maasserver/base.html (+0/-6)
src/maasserver/templates/maasserver/index.html (+0/-6)
src/maasserver/tests/test_bootsources.py (+10/-6)
src/maasserver/tests/test_components.py (+9/-20)
src/maasserver/views/combo.py (+1/-0)
src/maasserver/views/tests/test_general.py (+0/-36)
src/maastesting/karma.conf.js (+1/-0)
utilities/check-imports (+1/-0)
To merge this branch: bzr merge lp:~allenap/maas/notifications-creation
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+316718@code.launchpad.net

Commit message

Switch the persistent error mechanism to use notifications as its back-end.

Previously it was using ComponentError, which this change also removes.

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

Looks good. Other than the one question I have. Which actually might allow you to remove that new dependency you added.

We need to do some work to get the notifications to show on the none angular pages as well, since this branch removes notifications from those pages. Its not to hard to enable.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

> Looks good. Other than the one question I have. Which actually might
> allow you to remove that new dependency you added.

The new dependency is something we already pull in — python3-sphinx
depends on it, indirectly — so the net effect is zero. It'll only matter
if the dependencies of python3-sphinx change or if we stop depending on
python3-sphinx.

> We need to do some work to get the notifications to show on the none
> angular pages as well, since this branch removes notifications from
> those pages. Its not to hard to enable.

I'm interested to know how to do that.

Thanks!

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.7 MiB)

The attempt to merge lp:~allenap/maas/notifications-creation into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [102 kB]
Get:3 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
Get:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease [102 kB]
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates/main Sources [228 kB]
Get:6 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages [470 kB]
Get:7 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates/main Translation-en [187 kB]
Get:8 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates/universe amd64 Packages [396 kB]
Fetched 1,588 kB in 0s (2,706 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind avahi-utils bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common isc-dhcp-server libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libnss-wrapper libpq-dev make nodejs-legacy npm postgresql pxelinux python3-all python3-apt python3-attr python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~rc+dfsg-1ubuntu2).
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
git is already the newest version (1:2.7.4-0ubuntu1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5....

Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (5.0 KiB)

Unrelated test failure, as far as I can tell :-/ Nothing obviously wrong with maasserver.api.tests.test_node.TestPowerMixin.test_POST_test_tests_machine, I haven't been able to reproduce the failure locally, but it broke like so:

======================================================================
ERROR: maasserver.api.tests.test_node.TestPowerMixin.test_POST_test_tests_machine(user=user,client=oauth)
----------------------------------------------------------------------
Traceback (most recent call last):
testtools.testresult.real._StringException: Empty attachments:
  Twisted logs

Traceback (most recent call last):
  File "/tmp/tarmac/branch.a_baMO/src/maastesting/runtest.py", line 134, in _run_user
    result = function(*args, **kwargs)
  File "/home/ubuntu/.buildout/eggs/testtools-2.2.0-py3.5.egg/testtools/testcase.py", line 719, in _run_test_method
    return self._get_test_method()()
  File "/tmp/tarmac/branch.a_baMO/src/maasserver/api/tests/test_node.py", line 556, in test_POST_test_tests_machine
    response = self.client.post(self.get_node_uri(node), {'op': 'test'})
  File "/usr/lib/python3/dist-packages/django/test/client.py", line 512, in post
    secure=secure, **extra)
  File "/usr/lib/python3/dist-packages/django/test/client.py", line 313, in post
    secure=secure, **extra)
  File "/usr/lib/python3/dist-packages/django/test/client.py", line 379, in generic
    return self.request(**r)
  File "/tmp/tarmac/branch.a_baMO/src/maasserver/testing/testclient.py", line 140, in request
    return super(MAASSensibleOAuthClient, self).request(**kwargs)
  File "/tmp/tarmac/branch.a_baMO/src/maasserver/testing/testclient.py", line 49, in request
    return upcall(**request)
  File "/tmp/tarmac/branch.a_baMO/src/maasserver/utils/orm.py", line 562, in __exit__
    self.fire()
  File "/tmp/tarmac/branch.a_baMO/src/provisioningserver/utils/twisted.py", line 225, in wrapper
    result = func(*args, **kwargs)
  File "/tmp/tarmac/branch.a_baMO/src/maasserver/utils/async.py", line 218, in fire
    self._fire_in_reactor(hook).wait(LONGTIME)
  File "/usr/lib/python3/dist-packages/crochet/_eventloop.py", line 231, in wait
    result.raiseException()
  File "/usr/lib/python3/dist-packages/twisted/python/failure.py", line 368, in raiseException
    raise self.value.with_traceback(self.tb)
maasserver.models.node.DoesNotExist: Node matching query does not exist.

-------------------- >> begin captured logging << --------------------
maas.node: info: I5NT0fKaf40Y9ktd3YN5: Status transition from DEPLOYED to TESTING
maas.node: error: I5NT0fKaf40Y9ktd3YN5: Could not start testing for node: Node matching query does not exist.
--------------------- >> end captured logging << ---------------------
======================================================================
ERROR: maasserver.api.tests.test_node.TestPowerMixin.test_POST_test_tests_machine(user=user,client=user+pass)
----------------------------------------------------------------------
Traceback (most recent call last):
testtools.testresult.real._StringException: Empty attachments:
  Twisted logs

Traceback (most recent call last):
  File "/tmp/tarmac/branch.a_baMO/src/maastesting/runtest.py", line 1...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/bootsources.py'
--- src/maasserver/bootsources.py 2016-09-30 13:56:04 +0000
+++ src/maasserver/bootsources.py 2017-02-09 09:06:11 +0000
@@ -10,6 +10,7 @@
10 "get_os_info_from_boot_sources",10 "get_os_info_from_boot_sources",
11]11]
1212
13import html
13import os14import os
1415
15from maasserver.components import (16from maasserver.components import (
@@ -208,7 +209,8 @@
208 component = COMPONENT.REGION_IMAGE_IMPORT209 component = COMPONENT.REGION_IMAGE_IMPORT
209 if len(errors) > 0:210 if len(errors) > 0:
210 yield deferToDatabase(211 yield deferToDatabase(
211 register_persistent_error, component, "\n".join(errors))212 register_persistent_error, component,
213 "<br>".join(map(html.escape, errors)))
212 else:214 else:
213 yield deferToDatabase(215 yield deferToDatabase(
214 discard_persistent_error, component)216 discard_persistent_error, component)
215217
=== modified file 'src/maasserver/components.py'
--- src/maasserver/components.py 2016-06-30 16:49:02 +0000
+++ src/maasserver/components.py 2017-02-09 09:06:11 +0000
@@ -1,21 +1,25 @@
1# Copyright 2012-2016 Canonical Ltd. This software is licensed under the1# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""MAAS components management."""4"""MAAS components notifications.
5
6This is a legacy compatibility shim, to use the new notifications feature to
7display the old components messages. In time this will go away, but it's
8simple enough that there's no rush until it doesn't actually do what we want
9it to do.
10"""
511
6__all__ = [12__all__ = [
7 "discard_persistent_error",13 "discard_persistent_error",
8 "get_persistent_error",14 "get_persistent_error",
9 "get_persistent_errors",15 "get_persistent_errors",
10 "register_persistent_error",16 "register_persistent_error",
11 ]17]
1218
13from django.utils.safestring import mark_safe19from maasserver.enum import COMPONENT
14from maasserver.models import ComponentError20from maasserver.models import Notification
15from maasserver.utils.orm import (21from maasserver.utils.orm import transactional
16 get_one,22from provisioningserver.utils.enum import map_enum
17 transactional,
18)
1923
2024
21@transactional25@transactional
@@ -24,7 +28,7 @@
2428
25 :param component: An enum value of :class:`COMPONENT`.29 :param component: An enum value of :class:`COMPONENT`.
26 """30 """
27 ComponentError.objects.filter(component=component).delete()31 Notification.objects.filter(ident=component).delete()
2832
2933
30@transactional34@transactional
@@ -34,25 +38,31 @@
34 :param component: An enum value of :class:`COMPONENT`.38 :param component: An enum value of :class:`COMPONENT`.
35 :param error_message: Human-readable error text.39 :param error_message: Human-readable error text.
36 """40 """
37 component_error, created = ComponentError.objects.get_or_create(41 try:
38 component=component, defaults={'error': error_message})42 notification = Notification.objects.get(ident=component)
39 # If we didn't create a new object, we may need to update it if the error43 except Notification.DoesNotExist:
40 # message is different.44 notification = Notification.objects.create_error_for_admins(
41 if not created and component_error.error != error_message:45 error_message, ident=component)
42 component_error.error = error_message46 else:
43 component_error.save()47 if notification.message != error_message:
48 notification.message = error_message
49 notification.save()
4450
4551
46def get_persistent_error(component):52def get_persistent_error(component):
47 """Return persistent error for `component`, or None."""53 """Return persistent error for `component`, or None."""
48 err = get_one(ComponentError.objects.filter(component=component))54 try:
49 if err is None:55 notification = Notification.objects.get(ident=component)
56 except Notification.DoesNotExist:
50 return None57 return None
51 else:58 else:
52 return err.error59 return notification.render()
5360
5461
55def get_persistent_errors():62def get_persistent_errors():
56 """Return list of current persistent error messages."""63 """Return list of current persistent error messages."""
64 components = map_enum(COMPONENT).values()
57 return sorted(65 return sorted(
58 mark_safe(err.error) for err in ComponentError.objects.all())66 notification.render() for notification in
67 Notification.objects.filter(ident__in=components)
68 )
5969
=== modified file 'src/maasserver/context_processors.py'
--- src/maasserver/context_processors.py 2016-09-22 02:53:33 +0000
+++ src/maasserver/context_processors.py 2017-02-09 09:06:11 +0000
@@ -9,7 +9,6 @@
9 ]9 ]
1010
11from django.conf import settings11from django.conf import settings
12from maasserver.components import get_persistent_errors
13from maasserver.config import RegionConfiguration12from maasserver.config import RegionConfiguration
14from maasserver.models import Config13from maasserver.models import Config
15from maasserver.utils.version import (14from maasserver.utils.version import (
@@ -32,7 +31,6 @@
32 if hasattr(request.user, 'userprofile'):31 if hasattr(request.user, 'userprofile'):
33 user_completed_intro = request.user.userprofile.completed_intro32 user_completed_intro = request.user.userprofile.completed_intro
34 return {33 return {
35 'persistent_errors': get_persistent_errors(),
36 'global_options': {34 'global_options': {
37 'site_name': Config.objects.get_config('maas_name'),35 'site_name': Config.objects.get_config('maas_name'),
38 'enable_analytics': Config.objects.get_config('enable_analytics'),36 'enable_analytics': Config.objects.get_config('enable_analytics'),
3937
=== added file 'src/maasserver/migrations/builtin/maasserver/0111_remove_component_error.py'
--- src/maasserver/migrations/builtin/maasserver/0111_remove_component_error.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/builtin/maasserver/0111_remove_component_error.py 2017-02-09 09:06:11 +0000
@@ -0,0 +1,14 @@
1from django.db import migrations
2
3
4class Migration(migrations.Migration):
5
6 dependencies = [
7 ('maasserver', '0110_notification_category'),
8 ]
9
10 operations = [
11 migrations.DeleteModel(
12 name='ComponentError',
13 ),
14 ]
015
=== modified file 'src/maasserver/models/__init__.py'
--- src/maasserver/models/__init__.py 2017-01-23 19:56:16 +0000
+++ src/maasserver/models/__init__.py 2017-02-09 09:06:11 +0000
@@ -17,7 +17,6 @@
17 'BootSourceSelection',17 'BootSourceSelection',
18 'BridgeInterface',18 'BridgeInterface',
19 'CacheSet',19 'CacheSet',
20 'ComponentError',
21 'Config',20 'Config',
22 'Controller',21 'Controller',
23 'Device',22 'Device',
@@ -108,7 +107,6 @@
108from maasserver.models.bootsourcecache import BootSourceCache107from maasserver.models.bootsourcecache import BootSourceCache
109from maasserver.models.bootsourceselection import BootSourceSelection108from maasserver.models.bootsourceselection import BootSourceSelection
110from maasserver.models.cacheset import CacheSet109from maasserver.models.cacheset import CacheSet
111from maasserver.models.component_error import ComponentError
112from maasserver.models.config import Config110from maasserver.models.config import Config
113from maasserver.models.dhcpsnippet import DHCPSnippet111from maasserver.models.dhcpsnippet import DHCPSnippet
114from maasserver.models.discovery import Discovery112from maasserver.models.discovery import Discovery
115113
=== removed file 'src/maasserver/models/component_error.py'
--- src/maasserver/models/component_error.py 2015-12-01 18:12:59 +0000
+++ src/maasserver/models/component_error.py 1970-01-01 00:00:00 +0000
@@ -1,30 +0,0 @@
1# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Persistent component errors."""
5
6__all__ = [
7 'ComponentError',
8 ]
9
10
11from django.db.models import CharField
12from maasserver import DefaultMeta
13from maasserver.models.cleansave import CleanSave
14from maasserver.models.timestampedmodel import TimestampedModel
15
16
17class ComponentError(CleanSave, TimestampedModel):
18 """Error state of a major component of the system."""
19
20 class Meta(DefaultMeta):
21 """Needed for South to recognize this model."""
22
23 # A descriptor for the failing component, as in the COMPONENT enum.
24 # This is a failure state for an out-of-process component. We won't
25 # know much about what's wrong, and we don't support multiple errors
26 # for a single component.
27 component = CharField(max_length=40, unique=True, blank=False)
28
29 # Human-readable description of what's wrong.
30 error = CharField(max_length=1000, blank=False)
310
=== modified file 'src/maasserver/models/notification.py'
--- src/maasserver/models/notification.py 2017-02-02 17:10:32 +0000
+++ src/maasserver/models/notification.py 2017-02-09 09:06:11 +0000
@@ -23,6 +23,7 @@
23from maasserver.fields import JSONObjectField23from maasserver.fields import JSONObjectField
24from maasserver.models.cleansave import CleanSave24from maasserver.models.cleansave import CleanSave
25from maasserver.models.timestampedmodel import TimestampedModel25from maasserver.models.timestampedmodel import TimestampedModel
26from markupsafe import Markup
2627
2728
28def _create(method, category):29def _create(method, category):
@@ -172,8 +173,14 @@
172 )173 )
173174
174 def render(self):175 def render(self):
175 """Render this notification's message using its context."""176 """Render this notification's message using its context.
176 return self.message.format(**self.context)177
178 The message can contain HTML markup. Values from the context are
179 escaped.
180 """
181 markup = Markup(self.message)
182 markup = markup.format(**self.context)
183 return str(markup)
177184
178 def is_relevant_to(self, user):185 def is_relevant_to(self, user):
179 """Is this notification relevant to the given user?"""186 """Is this notification relevant to the given user?"""
180187
=== modified file 'src/maasserver/models/tests/test_notification.py'
--- src/maasserver/models/tests/test_notification.py 2017-02-02 17:10:32 +0000
+++ src/maasserver/models/tests/test_notification.py 2017-02-09 09:06:11 +0000
@@ -167,6 +167,13 @@
167 "There are " + str(thing_b) + " of " +167 "There are " + str(thing_b) + " of " +
168 thing_a + " in my suitcase."))168 thing_a + " in my suitcase."))
169169
170 def test_render_allows_markup_in_message_but_escapes_context(self):
171 message = "<foo>{bar}</foo>"
172 context = {"bar": "<BAR>"}
173 notification = Notification(message=message, context=context)
174 self.assertThat(
175 notification.render(), Equals("<foo>&lt;BAR&gt;</foo>"))
176
170 def test_save_checks_that_rendering_works(self):177 def test_save_checks_that_rendering_works(self):
171 message = "Dude, where's my {thing}?"178 message = "Dude, where's my {thing}?"
172 notification = Notification(message=message)179 notification = Notification(message=message)
173180
=== modified file 'src/maasserver/static/js/angular/directives/notifications.js'
--- src/maasserver/static/js/angular/directives/notifications.js 2017-02-03 17:03:51 +0000
+++ src/maasserver/static/js/angular/directives/notifications.js 2017-02-09 09:06:11 +0000
@@ -10,7 +10,7 @@
10 '<div ng-repeat="n in notifications" ng-class="classes[n.category]">',10 '<div ng-repeat="n in notifications" ng-class="classes[n.category]">',
11 '<p class="p-notification__response">',11 '<p class="p-notification__response">',
12 '<span class="p-notification__status"></span>',12 '<span class="p-notification__status"></span>',
13 '<span>{$ n.message $}</span> — ',13 '<span ng-bind-html="n.message"></span> — ',
14 '<a ng-click="dismiss(n)">Dismiss</a>',14 '<a ng-click="dismiss(n)">Dismiss</a>',
15 '<br><small>(id: {$ n.id $}, ',15 '<br><small>(id: {$ n.id $}, ',
16 'ident: {$ n.ident || "-" $}, user: {$ n.user || "-" $}, ',16 'ident: {$ n.ident || "-" $}, user: {$ n.user || "-" $}, ',
1717
=== modified file 'src/maasserver/static/js/angular/directives/tests/test_notifications.js'
--- src/maasserver/static/js/angular/directives/tests/test_notifications.js 2017-02-03 17:19:44 +0000
+++ src/maasserver/static/js/angular/directives/tests/test_notifications.js 2017-02-09 09:06:11 +0000
@@ -115,6 +115,19 @@
115 ]);115 ]);
116 });116 });
117117
118 it("sanitizes messages", function() {
119 var harmfulNotification = angular.copy(exampleNotifications[0]);
120 harmfulNotification.message =
121 "Hello <script>alert('Gotcha');</script><em>World</em>!";
122 theNotificationsManager._items = [harmfulNotification];
123 var directive = compileDirective();
124 var messages = directive.find("div > p");
125 expect(messages.html()).not.toContain("<script>");
126 expect(messages.html()).not.toContain("Gotcha");
127 expect(messages.html()).toContain("<em>World</em>");
128 expect(messages.text()).toContain("Hello World!");
129 });
130
118 });131 });
119132
120});133});
121134
=== modified file 'src/maasserver/static/js/angular/maas.js'
--- src/maasserver/static/js/angular/maas.js 2016-12-14 11:07:08 +0000
+++ src/maasserver/static/js/angular/maas.js 2017-02-09 09:06:11 +0000
@@ -9,7 +9,7 @@
9 */9 */
1010
11angular.module('MAAS',11angular.module('MAAS',
12 ['ngRoute', 'ngCookies', 'ngTagsInput', 'sticky']).config(12 ['ngRoute', 'ngCookies', 'ngSanitize', 'ngTagsInput', 'sticky']).config(
13 function($interpolateProvider, $routeProvider, $httpProvider) {13 function($interpolateProvider, $routeProvider, $httpProvider) {
14 $interpolateProvider.startSymbol('{$');14 $interpolateProvider.startSymbol('{$');
15 $interpolateProvider.endSymbol('$}');15 $interpolateProvider.endSymbol('$}');
1616
=== modified file 'src/maasserver/templates/maasserver/base.html'
--- src/maasserver/templates/maasserver/base.html 2017-01-27 11:49:08 +0000
+++ src/maasserver/templates/maasserver/base.html 2017-02-09 09:06:11 +0000
@@ -150,12 +150,6 @@
150 {% if user.is_authenticated %}150 {% if user.is_authenticated %}
151 <div class="row u-padding--top-none u-padding--bottom-none {% block notifications-class %}{% endblock %}">151 <div class="row u-padding--top-none u-padding--bottom-none {% block notifications-class %}{% endblock %}">
152 <div class="wrapper--inner">152 <div class="wrapper--inner">
153 {% for persistent_error in persistent_errors %}
154 <div class="p-notification--error">
155 <p class="p-notification__response">
156 <span class="p-notification__status">Error:</span>{{ persistent_error }}</p>
157 </div>
158 {% endfor %}
159 {% if messages %}153 {% if messages %}
160 {% for message in messages %}154 {% for message in messages %}
161 <div{% if message.tags %} class="p-notification p-notification--{{ message.tags }}" {% endif %}>155 <div{% if message.tags %} class="p-notification p-notification--{{ message.tags }}" {% endif %}>
162156
=== modified file 'src/maasserver/templates/maasserver/index.html'
--- src/maasserver/templates/maasserver/index.html 2017-01-13 15:42:46 +0000
+++ src/maasserver/templates/maasserver/index.html 2017-02-09 09:06:11 +0000
@@ -136,12 +136,6 @@
136 {% if user.is_authenticated %}136 {% if user.is_authenticated %}
137 <div class="wrapper--inner">137 <div class="wrapper--inner">
138 <div class="ng-hide">138 <div class="ng-hide">
139 {% for persistent_error in persistent_errors %}
140 <div class="p-notification--error">
141 <p class="p-notification__response">
142 <span class="p-notification__status">Error:</span>{{ persistent_error }}</p>
143 </div>
144 {% endfor %}
145 {% if messages %}139 {% if messages %}
146 {% for message in messages %}140 {% for message in messages %}
147 <div{% if message.tags %} class="p-notification--{{ message.tags }}" {% endif %}>141 <div{% if message.tags %} class="p-notification--{{ message.tags }}" {% endif %}>
148142
=== modified file 'src/maasserver/tests/test_bootsources.py'
--- src/maasserver/tests/test_bootsources.py 2016-09-30 13:56:04 +0000
+++ src/maasserver/tests/test_bootsources.py 2017-02-09 09:06:11 +0000
@@ -5,6 +5,7 @@
55
6__all__ = []6__all__ = []
77
8import html
8from os import environ9from os import environ
9from unittest import skip10from unittest import skip
10from unittest.mock import (11from unittest.mock import (
@@ -319,18 +320,21 @@
319 factory.make_BootSource(keyring_data=b'1234') for _ in range(3)]320 factory.make_BootSource(keyring_data=b'1234') for _ in range(3)]
320 download_image_descriptions = self.patch(321 download_image_descriptions = self.patch(
321 download_descriptions_module, 'download_image_descriptions')322 download_descriptions_module, 'download_image_descriptions')
322 error_text = factory.make_name("error_text")323 error_text_one = factory.make_name("<error1>")
324 error_text_two = factory.make_name("<error2>")
323 # Make two of the downloads fail.325 # Make two of the downloads fail.
324 download_image_descriptions.side_effect = [326 download_image_descriptions.side_effect = [
325 ConnectionError(error_text),327 ConnectionError(error_text_one),
326 BootImageMapping(),328 BootImageMapping(),
327 IOError(error_text),329 IOError(error_text_two),
328 ]330 ]
329 cache_boot_sources()331 cache_boot_sources()
330 base_error = "Failed to import images from boot source {url}: {err}"332 base_error = "Failed to import images from boot source {url}: {err}"
331 error_part_one = base_error.format(url=sources[0].url, err=error_text)333 error_part_one = base_error.format(
332 error_part_two = base_error.format(url=sources[2].url, err=error_text)334 url=sources[0].url, err=html.escape(error_text_one))
333 expected_error = error_part_one + '\n' + error_part_two335 error_part_two = base_error.format(
336 url=sources[2].url, err=html.escape(error_text_two))
337 expected_error = error_part_one + '<br>' + error_part_two
334 actual_error = get_persistent_error(COMPONENT.REGION_IMAGE_IMPORT)338 actual_error = get_persistent_error(COMPONENT.REGION_IMAGE_IMPORT)
335 self.assertEqual(expected_error, actual_error)339 self.assertEqual(expected_error, actual_error)
336340
337341
=== renamed file 'src/maasserver/models/tests/test_components.py' => 'src/maasserver/tests/test_components.py'
--- src/maasserver/models/tests/test_components.py 2016-06-30 16:49:02 +0000
+++ src/maasserver/tests/test_components.py 2017-02-09 09:06:11 +0000
@@ -10,12 +10,11 @@
1010
11from maasserver.components import (11from maasserver.components import (
12 discard_persistent_error,12 discard_persistent_error,
13 get_persistent_error,
14 get_persistent_errors,13 get_persistent_errors,
15 register_persistent_error,14 register_persistent_error,
16)15)
17from maasserver.enum import COMPONENT16from maasserver.enum import COMPONENT
18from maasserver.models.component_error import ComponentError17from maasserver.models import Notification
19from maasserver.testing.factory import factory18from maasserver.testing.factory import factory
20from maasserver.testing.testcase import MAASServerTestCase19from maasserver.testing.testcase import MAASServerTestCase
21from provisioningserver.utils.enum import map_enum20from provisioningserver.utils.enum import map_enum
@@ -27,9 +26,6 @@
2726
28class PersistentErrorsUtilitiesTest(MAASServerTestCase):27class PersistentErrorsUtilitiesTest(MAASServerTestCase):
2928
30 def setUp(self):
31 super(PersistentErrorsUtilitiesTest, self).setUp()
32
33 def test_register_persistent_error_registers_error(self):29 def test_register_persistent_error_registers_error(self):
34 error_message = factory.make_string()30 error_message = factory.make_string()
35 component = get_random_component()31 component = get_random_component()
@@ -70,15 +66,6 @@
70 components.append(component)66 components.append(component)
71 self.assertItemsEqual(errors, get_persistent_errors())67 self.assertItemsEqual(errors, get_persistent_errors())
7268
73 def test_get_persistent_error_returns_None_if_no_error(self):
74 self.assertIsNone(get_persistent_error(factory.make_name('component')))
75
76 def test_get_persistent_error_returns_component_error(self):
77 component = factory.make_name('component')
78 error = factory.make_name('error')
79 register_persistent_error(component, error)
80 self.assertEqual(error, get_persistent_error(component))
81
82 def test_register_persistent_error_reuses_component_errors(self):69 def test_register_persistent_error_reuses_component_errors(self):
83 """When registering a persistent error that already has an error70 """When registering a persistent error that already has an error
84 recorded for that component, reuse the error instead of deleting and71 recorded for that component, reuse the error instead of deleting and
@@ -87,10 +74,12 @@
87 error1 = factory.make_name('error')74 error1 = factory.make_name('error')
88 error2 = factory.make_name('error')75 error2 = factory.make_name('error')
89 register_persistent_error(component, error1)76 register_persistent_error(component, error1)
90 error = ComponentError.objects.get(component=component)77 notification = Notification.objects.get(ident=component)
91 self.assertEqual(error.error, error1) # Should be our error78 self.assertEqual(notification.render(), error1)
92 error_id = error.id79 notification_id = notification.id
93 register_persistent_error(component, error2)80 register_persistent_error(component, error2)
94 error = ComponentError.objects.get(component=component)81 notification = Notification.objects.get(ident=component)
95 self.assertEqual(error.error, error2) # Should update the message82 # The message is updated.
96 self.assertEqual(error.id, error_id) # Should reuse the same id83 self.assertEqual(notification.render(), error2)
84 # The same notification row is used.
85 self.assertEqual(notification.id, notification_id)
9786
=== modified file 'src/maasserver/views/combo.py'
--- src/maasserver/views/combo.py 2017-02-01 22:18:19 +0000
+++ src/maasserver/views/combo.py 2017-02-09 09:06:11 +0000
@@ -39,6 +39,7 @@
39 "angular.min.js",39 "angular.min.js",
40 "angular-route.min.js",40 "angular-route.min.js",
41 "angular-cookies.min.js",41 "angular-cookies.min.js",
42 "angular-sanitize.min.js",
42 ]43 ]
43 },44 },
44 "ng-tags-input.js": {45 "ng-tags-input.js": {
4546
=== modified file 'src/maasserver/views/tests/test_general.py'
--- src/maasserver/views/tests/test_general.py 2016-12-12 21:40:28 +0000
+++ src/maasserver/views/tests/test_general.py 2017-02-09 09:06:11 +0000
@@ -6,22 +6,17 @@
6__all__ = []6__all__ = []
77
8import http.client8import http.client
9from random import randint
10from urllib.parse import (9from urllib.parse import (
11 parse_qs,10 parse_qs,
12 urlparse,11 urlparse,
13)12)
14from xmlrpc.client import Fault
1513
16from django.conf import settings14from django.conf import settings
17from django.conf.urls import patterns15from django.conf.urls import patterns
18from django.core.exceptions import PermissionDenied16from django.core.exceptions import PermissionDenied
19from django.core.urlresolvers import reverse
20from django.http import Http40417from django.http import Http404
21from django.test.client import RequestFactory18from django.test.client import RequestFactory
22from django.utils.html import escape
23from lxml.html import fromstring19from lxml.html import fromstring
24from maasserver.components import register_persistent_error
25from maasserver.testing import extract_redirect20from maasserver.testing import extract_redirect
26from maasserver.testing.factory import factory21from maasserver.testing.factory import factory
27from maasserver.testing.testcase import MAASServerTestCase22from maasserver.testing.testcase import MAASServerTestCase
@@ -29,7 +24,6 @@
29 HelpfulDeleteView,24 HelpfulDeleteView,
30 PaginatedListView,25 PaginatedListView,
31)26)
32from testtools.matchers import ContainsAll
3327
3428
35class Test404500(MAASServerTestCase):29class Test404500(MAASServerTestCase):
@@ -313,33 +307,3 @@
313 "page": ["4"],307 "page": ["4"],
314 },308 },
315 parse_qs(urlparse(context["last_page_link"]).query))309 parse_qs(urlparse(context["last_page_link"]).query))
316
317
318class PermanentErrorDisplayTest(MAASServerTestCase):
319
320 def test_permanent_error_displayed(self):
321 self.client_log_in()
322 fault_codes = [
323 randint(1, 100),
324 randint(101, 200),
325 ]
326 errors = []
327 for fault in fault_codes:
328 # Create component with make_string to be sure to display all
329 # the errors.
330 component = factory.make_name('component')
331 error_message = factory.make_name('error')
332 errors.append(Fault(fault, error_message))
333 register_persistent_error(component, error_message)
334 links = [
335 reverse('index'),
336 reverse('prefs'),
337 ]
338 for link in links:
339 response = self.client.get(link)
340 self.assertThat(
341 response.content,
342 ContainsAll([
343 escape(error.faultString).encode(settings.DEFAULT_CHARSET)
344 for error in errors
345 ]))
346310
=== modified file 'src/maastesting/karma.conf.js'
--- src/maastesting/karma.conf.js 2015-03-26 17:55:20 +0000
+++ src/maastesting/karma.conf.js 2017-02-09 09:06:11 +0000
@@ -20,6 +20,7 @@
20 '/usr/share/javascript/angular.js/angular-route.js',20 '/usr/share/javascript/angular.js/angular-route.js',
21 '/usr/share/javascript/angular.js/angular-mocks.js',21 '/usr/share/javascript/angular.js/angular-mocks.js',
22 '/usr/share/javascript/angular.js/angular-cookies.js',22 '/usr/share/javascript/angular.js/angular-cookies.js',
23 '/usr/share/javascript/angular.js/angular-sanitize.js',
23 '../../src/maasserver/static/js/angular/maas.js',24 '../../src/maasserver/static/js/angular/maas.js',
24 '../../src/maasserver/static/js/angular/testing/*.js',25 '../../src/maasserver/static/js/angular/testing/*.js',
25 '../../src/maasserver/static/js/angular/*/*.js',26 '../../src/maasserver/static/js/angular/*/*.js',
2627
=== modified file 'utilities/check-imports'
--- utilities/check-imports 2017-02-04 21:59:38 +0000
+++ utilities/check-imports 2017-02-09 09:06:11 +0000
@@ -245,6 +245,7 @@
245 Allow("lxml|lxml.**"),245 Allow("lxml|lxml.**"),
246 Allow("maascli.utils.parse_docstring"),246 Allow("maascli.utils.parse_docstring"),
247 Allow("maasserver|maasserver.**"),247 Allow("maasserver|maasserver.**"),
248 Allow("markupsafe|markupsafe.**"),
248 Allow("metadataserver|metadataserver.**"),249 Allow("metadataserver|metadataserver.**"),
249 Allow("netaddr|netaddr.**"),250 Allow("netaddr|netaddr.**"),
250 Allow("oauth|oauth.**"),251 Allow("oauth|oauth.**"),