diff --git a/src/tests/ui_test.py b/src/tests/ui_test.py index f038b261e..462483aa0 100644 --- a/src/tests/ui_test.py +++ b/src/tests/ui_test.py @@ -97,6 +97,7 @@ def test_getclu(self): resp = self.app.get('/getclu/1', headers={'Content-Type': 'application/json'}) self.assertEquals(200, resp.status_code) + assert 'notifications' in json.loads(resp.data.decode('utf8')) self.app.get('/logout') self.app.post('/login', data={'login': 'user2', 'password': 'user2'}) resp = self.app.get('/getclu/1', diff --git a/src/web/js/actions/MenuActions.js b/src/web/js/actions/MenuActions.js index 00006a4fc..9cb804cb1 100644 --- a/src/web/js/actions/MenuActions.js +++ b/src/web/js/actions/MenuActions.js @@ -10,17 +10,8 @@ var MenuActions = { reload: function(set_filter, setFilterFunc, id) { jquery.getJSON('/menu', function(payload) { var old_all_unread_count = MenuStore.all_unread_count; - JarrDispatcher.dispatch({ - type: ActionTypes.RELOAD_MENU, - feeds: payload.feeds, - categories: payload.categories, - categories_order: payload.categories_order, - is_admin: payload.is_admin, - max_error: payload.max_error, - error_threshold: payload.error_threshold, - crawling_method: payload.crawling_method, - all_unread_count: payload.all_unread_count, - }); + payload.type = ActionTypes.RELOAD_MENU; + JarrDispatcher.dispatch(payload); /* setfilter param is here so were sure it's called in the sole * purpose of setting filter and that the setFilterFunc is not * some event passed by react diff --git a/src/web/js/actions/RightPanelActions.js b/src/web/js/actions/RightPanelActions.js index 9f3707825..92308b707 100644 --- a/src/web/js/actions/RightPanelActions.js +++ b/src/web/js/actions/RightPanelActions.js @@ -24,6 +24,7 @@ var RightPanelActions = { JarrDispatcher.dispatch({ type: ActionTypes.LOAD_CLUSTER, cluster: payload, + notifications: payload.notifications, was_read_before: was_read_before, article_id: article_id, }); diff --git a/src/web/js/components/MainApp.react.js b/src/web/js/components/MainApp.react.js index 7944a00b3..486049c5d 100644 --- a/src/web/js/components/MainApp.react.js +++ b/src/web/js/components/MainApp.react.js @@ -5,6 +5,7 @@ var JarrNavBar = require('./Navbar.react'); var Menu = require('./Menu.react'); var MiddlePanel = require('./MiddlePanel.react'); var RightPanel = require('./RightPanel.react'); +var Notifications = require('./Notifications.react'); var MainApp = React.createClass({ @@ -16,6 +17,7 @@ var MainApp = React.createClass({ + ); }, diff --git a/src/web/js/components/Notifications.react.js b/src/web/js/components/Notifications.react.js new file mode 100644 index 000000000..8426a65b3 --- /dev/null +++ b/src/web/js/components/Notifications.react.js @@ -0,0 +1,46 @@ +var React = require('react'); +var NotificationsStore = require('../stores/NotificationsStore'); +var NotificationSystem = require('react-notification-system'); + + +var Notifications = React.createClass({ + _notificationSystem: null, + addNotification: function(notif) { + this._notificationSystem.addNotification({ + message: notif.message, + level: notif.level, + autoDismiss: 30, + onRemove: this.removeNotification, + }); + }, + removeNotification: function(notif) { + for(var idx in NotificationsStore.notifs) { + if(NotificationsStore.notifs[idx].read == false + && NotificationsStore.notifs[idx].level == notif.level + && NotificationsStore.notifs[idx].message == notif.message) { + NotificationsStore.notifs[idx].read = true; + break; + } + + } + }, + render: function() { + return ; + }, + componentDidMount: function() { + this._notificationSystem = this.refs.notificationSystem; + NotificationsStore.addChangeListener(this._onChange); + }, + componentWillUnmount: function() { + NotificationsStore.removeChangeListener(this._onChange); + }, + _onChange: function() { + for(var idx in NotificationsStore.notifs) { + if(!NotificationsStore.notifs[idx].read) { + this.addNotification(NotificationsStore.notifs[idx]); + } + } + }, +}); + +module.exports = Notifications; diff --git a/src/web/js/stores/NotificationsStore.js b/src/web/js/stores/NotificationsStore.js new file mode 100644 index 000000000..8f9613745 --- /dev/null +++ b/src/web/js/stores/NotificationsStore.js @@ -0,0 +1,50 @@ +var JarrDispatcher = require('../dispatcher/JarrDispatcher'); +var ActionTypes = require('../constants/JarrConstants'); +var EventEmitter = require('events').EventEmitter; + +var CHANGE_EVENT = 'change_menu'; +var assign = require('object-assign'); + + +var NotificationsStore = assign({}, EventEmitter.prototype, { + notifs: [], + + addNotifications: function(notifications) { + var count = this.notifs.length; + for(var idx in notifications) { + this.notifs.push({ + key: parseInt(idx) + count, + read: false, + level: notifications[idx].level, + message: notifications[idx].message, + }); + } + }, + getNotifications: function() { + this.notifs = this.notifs.filter(function(notif) {return !notif.read;}); + return this.notifs; + }, + emitChange: function(all_folded) { + if (all_folded) { + this.all_folded = all_folded; + } else { + this.all_folded = null; + } + this.emit(CHANGE_EVENT); + }, + addChangeListener: function(callback) { + this.on(CHANGE_EVENT, callback); + }, + removeChangeListener: function(callback) { + this.removeListener(CHANGE_EVENT, callback); + }, +}); + +NotificationsStore.dispatchToken = JarrDispatcher.register(function(action) { + if(action.notifications) { + NotificationsStore.addNotifications(action.notifications); + NotificationsStore.emitChange(); + } +}); + +module.exports = NotificationsStore; diff --git a/src/web/lib/view_utils.py b/src/web/lib/view_utils.py index 676a513c5..642470fce 100644 --- a/src/web/lib/view_utils.py +++ b/src/web/lib/view_utils.py @@ -1,12 +1,14 @@ import pytz from functools import wraps from datetime import datetime -from flask import request, Response, make_response +from flask import request, Response, make_response, get_flashed_messages from flask_babel import get_locale from babel.dates import format_datetime, format_timedelta from web.views.common import jsonify from lib.utils import to_hash +ACCEPTED_LEVELS = {'success', 'info', 'warning', 'error'} + def etag_match(func): @wraps(func) @@ -41,7 +43,14 @@ def _iter_on_rows(rows, now, locale): yield row +def get_notifications(): + for msg in get_flashed_messages(with_categories=True): + yield {'level': msg[0] if msg[0] in ACCEPTED_LEVELS else 'info', + 'message': msg[1]} + + @jsonify def clusters_to_json(clusters): return {'clusters': _iter_on_rows(clusters, - datetime.utcnow(), get_locale())} + datetime.utcnow(), get_locale()), + 'notifications': get_notifications()} diff --git a/src/web/views/admin.py b/src/web/views/admin.py index 1103635b2..baa2b3d4b 100644 --- a/src/web/views/admin.py +++ b/src/web/views/admin.py @@ -50,7 +50,7 @@ def user(user_id=None): unread_counts=clu_contr.count_by_feed(read=False)) else: - flash(gettext('This user does not exist.'), 'warn') + flash(gettext('This user does not exist.'), 'warning') return redirect(redirect_url()) @@ -67,7 +67,7 @@ def toggle_user(user_id=None): {'is_active': not user.is_active}) if not user_changed: - flash(gettext('This user does not exist.'), 'danger') + flash(gettext('This user does not exist.'), 'error') return redirect(url_for('admin.dashboard')) else: diff --git a/src/web/views/feed.py b/src/web/views/feed.py index 050250858..0c060abd2 100644 --- a/src/web/views/feed.py +++ b/src/web/views/feed.py @@ -1,5 +1,4 @@ import logging -import requests.exceptions from werkzeug.exceptions import BadRequest from flask import Blueprint, render_template, flash, \ diff --git a/src/web/views/home.py b/src/web/views/home.py index 49187fb27..2f2583386 100644 --- a/src/web/views/home.py +++ b/src/web/views/home.py @@ -9,7 +9,7 @@ from bootstrap import conf from web.lib.article_cleaner import clean_urls -from web.lib.view_utils import etag_match, clusters_to_json +from web.lib.view_utils import etag_match, clusters_to_json, get_notifications from web.views.common import jsonify from web.controllers import (UserController, CategoryController, @@ -71,6 +71,7 @@ def get_menu(): 'max_error': conf.FEED_ERROR_MAX, 'error_threshold': conf.FEED_ERROR_THRESHOLD, 'is_admin': current_user.is_admin, + 'notifications': get_notifications(), 'all_unread_count': 0} @@ -144,7 +145,7 @@ def get_cluster(cluster_id, parse=False, article_id=None): new_content = clean_urls(new_content, article['link'], fix_readability=True) except Exception as error: - flash("Readability failed with %r" % error, "danger") + flash("Readability failed with %r" % error, "warning") article['readability_parsed'] = False else: article['readability_parsed'] = True @@ -153,6 +154,7 @@ def get_cluster(cluster_id, parse=False, article_id=None): {'readability_parsed': True, 'content': new_content}) for article in cluster.articles: article['readability_available'] = readability_available + cluster['notifications'] = get_notifications() return cluster diff --git a/src/web/views/user.py b/src/web/views/user.py index b883ab9a4..dae1222ef 100644 --- a/src/web/views/user.py +++ b/src/web/views/user.py @@ -45,7 +45,7 @@ def opml_import(): try: subscriptions = opml.from_string(data.read()) except: - flash(gettext("Couldn't parse file"), 'danger') + flash(gettext("Couldn't parse file"), 'error') return redirect(request.referrer) ccontr = CategoryController(current_user.id) diff --git a/src/web/views/views.py b/src/web/views/views.py index 7588ea998..3ce3e1804 100644 --- a/src/web/views/views.py +++ b/src/web/views/views.py @@ -21,7 +21,7 @@ def authentication_required(error): def authentication_failed(error): if conf.API_ROOT in request.url: return error - flash(gettext('Forbidden.'), 'danger') + flash(gettext('Forbidden.'), 'error') return redirect(url_for('login'))