Skip to content

Commit 1faec96

Browse files
committed
add wrapper for Django messages framework
1 parent 333cea9 commit 1faec96

File tree

7 files changed

+232
-1
lines changed

7 files changed

+232
-1
lines changed

shop/messages.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
A wrapper around Django's messages framework for easier integration with Javascript based messages.
4+
"""
5+
from __future__ import unicode_literals
6+
7+
import json
8+
from django.contrib import messages as django_messages
9+
from django.utils.text import force_text
10+
11+
12+
def add_message(request, level, message, title=None, delay=None):
13+
if title is None:
14+
title = django_messages.DEFAULT_TAGS[level].capitalize()
15+
extra_tags = {'title': force_text(title), 'delay': delay}
16+
django_messages.add_message(request, level, force_text(message), extra_tags=json.dumps(extra_tags))
17+
18+
19+
def success(request, message, title=None, delay=0):
20+
add_message(request, django_messages.SUCCESS, message, title, delay)
21+
22+
23+
def warning(request, message, title=None, delay=0):
24+
add_message(request, django_messages.WARNING, message, title, delay)
25+
26+
27+
def error(request, message, title=None, delay=0):
28+
add_message(request, django_messages.ERROR, message, title, delay)
29+
30+
31+
def info(request, message, title=None, delay=0):
32+
add_message(request, django_messages.INFO, message, title, delay)
33+
34+
35+
def debug(request, message, title=None, delay=0):
36+
add_message(request, django_messages.DEBUG, message, title, delay)
37+
38+
39+
def get_messages_as_json(request):
40+
data = []
41+
for message in django_messages.get_messages(request):
42+
try:
43+
extra_tags = json.loads(message.extra_tags)
44+
except (TypeError, json.JSONDecodeError):
45+
extra_tags = {}
46+
heading = extra_tags.get('title', message.level_tag.capitalize())
47+
try:
48+
delay = int(float(extra_tags['delay']) * 1000)
49+
except (KeyError, ValueError):
50+
delay = None
51+
data.append({
52+
'level': message.level_tag,
53+
'heading': heading,
54+
'body': message.message,
55+
'delay': delay,
56+
})
57+
return data
+4
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
@import "bootstrap/scss/bootstrap";
22
@import "django-angular";
3+
4+
shop-toast-messages {
5+
z-index: $zindex-modal;
6+
}

shop/static/shop/js/cart.js

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ djangoShopModule.directive('shopCartItem', function() {
5858
data: cartItem
5959
}).then(function(response) {
6060
$rootScope.$broadcast('shop.cart.change', response.data.cart);
61+
$rootScope.$broadcast('shop.messages.fetch');
6162
deferred.resolve(response);
6263
});
6364
return deferred.promise;

shop/static/shop/js/messages.js

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
(function(angular, undefined) {
2+
'use strict';
3+
4+
var djangoShopModule = angular.module('django.shop.messages', []);
5+
6+
7+
djangoShopModule.provider('djangoMessages', function() {
8+
var endpointURL;
9+
10+
this.alertLevelMap = {
11+
'debug': 'bg-secondary text-white',
12+
'info': 'bg-info text-white',
13+
'success': 'bg-success text-white',
14+
'warning': 'bg-warning text-dark',
15+
'error': 'bg-danger text-white',
16+
};
17+
this.alertIconMap = {
18+
'debug': 'fa-bug',
19+
'info': 'fa-info-circle',
20+
'success': 'fa-check-circle',
21+
'warning': 'fa-exclamation-circle',
22+
'error': 'fa-exclamation-triangle',
23+
};
24+
25+
this.setEndpoint = function(url) {
26+
endpointURL = url;
27+
};
28+
29+
this.$get = ['$http', '$rootScope', '$timeout', function($http, $rootScope, $timeout) {
30+
var self = this;
31+
$rootScope.$on('shop.messages.fetch', fetchNewMessages);
32+
$timeout(function() {
33+
fetchNewMessages();
34+
});
35+
36+
function fetchNewMessages() {
37+
$http.get(endpointURL).then(function(response) {
38+
$rootScope.djangoMessages = angular.isArray($rootScope.djangoMessages) ? $rootScope.djangoMessages : [];
39+
angular.extend($rootScope.djangoMessages, response.data.django_messages);
40+
angular.forEach($rootScope.djangoMessages, function(message) {
41+
if (message.delay > 0 && angular.isUndefined(message.timeout)) {
42+
message.timeout = $timeout(function() {
43+
self.removeMessage(message);
44+
}, message.delay);
45+
}
46+
});
47+
});
48+
}
49+
50+
this.removeMessage = function(message) {
51+
message.hide = true;
52+
$timeout(function() {
53+
var index = $rootScope.djangoMessages.indexOf(message);
54+
if (index >= 0) {
55+
$rootScope.djangoMessages.splice(index, 1);
56+
}
57+
}, 250);
58+
};
59+
60+
return this;
61+
}];
62+
});
63+
64+
65+
djangoShopModule.directive('shopToastMessages', ['$rootScope', 'djangoMessages', function($rootScope, djangoMessages) {
66+
return {
67+
restrict: 'E',
68+
scope: true,
69+
templateUrl: 'shop/toast-messages.html',
70+
link: function(scope, element, attrs) {
71+
scope.alertLevel = function(message) {
72+
return djangoMessages.alertLevelMap[message.level] || 'text-dark';
73+
};
74+
scope.alertIcon = function(message) {
75+
return djangoMessages.alertIconMap[message.level] || 'fa-circle-o';
76+
};
77+
scope.appear = function(message) {
78+
if (!message.hide) {
79+
return 'show';
80+
}
81+
};
82+
scope.close = djangoMessages.removeMessage;
83+
}
84+
};
85+
}]);
86+
87+
88+
/*
89+
djangoShopModule.directive('toast', ['$timeout', function($timeout) {
90+
return {
91+
restrict: 'C',
92+
scope: true,
93+
link: function(scope, element, attrs) {
94+
$timeout(function() {
95+
element.addClass('show');
96+
}, 500);
97+
scope.close = function() {
98+
element.removeClass('show');
99+
$timeout(function() {
100+
element.addClass('hide');
101+
}, 500);
102+
};
103+
scope.alertLevel = function(levelTag) {
104+
switch (levelTag) {
105+
case 'debug': return 'bg-secondary text-white';
106+
case 'info': return 'bg-info text-white';
107+
case 'success': return 'bg-success text-white';
108+
case 'warning': return 'bg-warning text-dark';
109+
case 'error': return 'bg-danger text-white';
110+
default: return 'text-dark';
111+
}
112+
};
113+
scope.alertIcon = function(levelTag) {
114+
switch (levelTag) {
115+
case 'debug': return 'fa-bug';
116+
case 'info': return 'fa-info-circle';
117+
case 'success': return 'fa-check-circle';
118+
case 'warning': return 'fa-exclamation-circle';
119+
case 'error': return 'fa-exclamation-triangle';
120+
default: return 'fa-circle-o';
121+
}
122+
};
123+
if (parseInt(attrs.delay)) {
124+
$timeout(scope.close, attrs.delay);
125+
}
126+
}
127+
};
128+
}]);
129+
*/
130+
131+
})(window.angular);

shop/static/shop/js/purchase-button.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ var module = angular.module('django.shop.purchase-button', ['djng.forms']);
99
// snippet, containing instructions on what to do next. Typically this is another http request onto
1010
// the PSP's endpoint. Sometimes it is a redirect onto another page, hence the `$window` parameter
1111
// has to be injected as well, even though unused.
12-
module.directive('button', ['$http', '$log', '$q', '$window', function($http, $log, $q, $window) {
12+
module.directive('button', ['$http', '$log', '$q', '$rootScope', '$window', function($http, $log, $q, $rootScope, $window) {
1313
return {
1414
restrict: 'E',
1515
require: '^?djngFormsSet',
@@ -29,6 +29,8 @@ module.directive('button', ['$http', '$log', '$q', '$window', function($http, $l
2929
scope.purchasingErrorMessage = response.data.purchasing_error_message;
3030
}
3131
deferred.reject(response);
32+
}).finally(function() {
33+
$rootScope.$broadcast('shop.messages.fetch');
3234
});
3335
return deferred.promise;
3436
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{% load i18n static sekizai_tags %}{% spaceless %}
2+
3+
{% addtoblock "js" %}<script src="{% static 'shop/js/messages.js' %}" type="text/javascript"></script>{% endaddtoblock %}
4+
{% add_data "ng-requires" "django.shop.messages" %}
5+
{% addtoblock "ng-config" %}['djangoMessagesProvider',function(p){p.setEndpoint("{% url 'shop:fetch-messages' %}");}]{% endaddtoblock %}
6+
7+
<shop-toast-messages class="position-fixed w-100 d-flex mt-2"></shop-toast-messages>
8+
9+
{% verbatim %}
10+
<script id="shop/toast-messages.html" type="text/ng-template">
11+
<div class="mx-auto d-flex flex-column">
12+
<div ng-repeat="message in djangoMessages" class="toast fade" ng-class="appear(message)" role="alert" aria-live="assertive" aria-atomic="true" delay="{{ message.delay }}">
13+
<div class="toast-header" ng-class="alertLevel(message)">
14+
<i class="fa mr-2" ng-class="alertIcon(message)" aria-hidden="true"></i>
15+
<strong class="mr-auto">{{ message.heading }}</strong>
16+
<button type="button" class="ml-2 mb-1 close" ng-click="close(message)" aria-label="{% trans 'Close' %}">
17+
<span aria-hidden="true" ng-click="">&times;</span>
18+
</button>
19+
</div>
20+
<div class="toast-body">{{ message.body }}</div>
21+
</div>
22+
</div>
23+
</script>
24+
{% endverbatim %}
25+
{% endspaceless %}

shop/urls/rest_api.py

+11
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
from __future__ import unicode_literals
33

44
from django.conf.urls import url, include
5+
from django.http import JsonResponse
56
from rest_framework import routers
67

78
from shop.forms.checkout import ShippingAddressForm, BillingAddressForm
9+
from shop.messages import get_messages_as_json
810
from shop.views.address import AddressEditView
911
from shop.views.cart import CartViewSet, WatchViewSet
1012
from shop.views.checkout import CheckoutViewSet
@@ -15,10 +17,19 @@
1517
router.register(r'watch', WatchViewSet, base_name='watch')
1618
router.register(r'checkout', CheckoutViewSet, base_name='checkout')
1719

20+
21+
def fetch_messages(request):
22+
data = get_messages_as_json(request)
23+
return JsonResponse({'django_messages': data})
24+
25+
1826
urlpatterns = [
1927
url(r'^select_product/?$',
2028
ProductSelectView.as_view(),
2129
name='select-product'),
30+
url(r'^fetch_messages/?$',
31+
fetch_messages,
32+
name='fetch-messages'),
2233
url(r'^shipping_address/(?P<priority>({{\s*\w+\s*}}|\d+|add))$',
2334
AddressEditView.as_view(form_class=ShippingAddressForm),
2435
name='edit-shipping-address'),

0 commit comments

Comments
 (0)