PHRAS-3442_optimize-list-notifications_4.1-bis

WIP
poll only from menubar (Aina)
back : only one method/route to fetch notifs
front : fix exponential pagination
big refacto
todo : mark read notifications (button ? click ?)
This commit is contained in:
jygaulier
2021-06-15 17:46:14 +02:00
parent eec8a92ce9
commit 22c3b273f7
24 changed files with 710 additions and 628 deletions

View File

@@ -1,7 +1,7 @@
import $ from 'jquery'; import $ from 'jquery';
import ui from '../ui';
import notify from '../notify';
// poll notification is now from menu bar
// so this is never called
const basket = () => { const basket = () => {
const onUpdatedContent = (data) => { const onUpdatedContent = (data) => {

View File

@@ -4,6 +4,8 @@ import notifyService from './notifyService';
import * as Rx from 'rx'; import * as Rx from 'rx';
import merge from 'lodash.merge'; import merge from 'lodash.merge';
// this module is now unused, poll notification is from menu bar
const notify = (services) => { const notify = (services) => {
const { configService, localeService, appEvents } = services; const { configService, localeService, appEvents } = services;
@@ -63,9 +65,9 @@ const notify = (services) => {
appEvents.emit('notification.refresh', data); appEvents.emit('notification.refresh', data);
} }
// append notification content // append notification content
notifyLayout(services).addNotifications(data.notifications); // notifyLayout(services).addNotifications(data.notifications);
window.setTimeout(poll, defaultPollingTime, notificationInstance); // window.setTimeout(poll, defaultPollingTime, notificationInstance);
return true; return true;
}; };

View File

@@ -1,5 +1,4 @@
import $ from 'jquery'; import $ from 'jquery';
import dialog from './../../phraseanet-common/components/dialog';
// import user from '../user/index.js'; // import user from '../user/index.js';
@@ -8,34 +7,34 @@ const notifyLayout = (services) => {
const $notificationBoxContainer = $('#notification_box'); const $notificationBoxContainer = $('#notification_box');
const $notificationTrigger = $('.notification_trigger'); const $notificationTrigger = $('.notification_trigger');
let $notificationDialog = $('#notifications-dialog'); let $notificationDialog = $('#notifications-dialog');
let $notificationsContent = null;
let $notificationsNavigation = null;
const initialize = () => { const initialize = () => {
/**
* click on menubar/notifications : drop a box with last 10 notification, and a button "see all"
* the box content is already set by poll notifications
*/
$notificationTrigger.on('mousedown', (event) => { $notificationTrigger.on('mousedown', (event) => {
event.stopPropagation(); event.stopPropagation();
const $target = $(event.currentTarget); // toggle
if ($target.hasClass('open')) {
$notificationBoxContainer.hide();
$target.removeClass('open');
clear_notifications();
} else {
$notificationBoxContainer.show();
setBoxHeight();
$target.addClass('open');
read_notifications();
}
});
$(document).on('mousedown', () => {
if ($notificationTrigger.hasClass('open')) {
$notificationTrigger.trigger('click');
}
if ($notificationTrigger.hasClass('open')) { if ($notificationTrigger.hasClass('open')) {
$notificationBoxContainer.hide(); $notificationBoxContainer.hide();
$notificationTrigger.removeClass('open'); $notificationTrigger.removeClass('open');
clear_notifications();
} }
else {
$notificationTrigger.addClass('open');
$notificationBoxContainer.show();
setBoxHeight();
}
});
/**
* close on every mousedown
*/
$(document).on('mousedown', () => {
$notificationBoxContainer.hide();
$notificationTrigger.removeClass('open');
}); });
$notificationBoxContainer $notificationBoxContainer
@@ -48,11 +47,14 @@ const notifyLayout = (services) => {
.on('mouseout', '.notification', (event) => { .on('mouseout', '.notification', (event) => {
$(event.currentTarget).removeClass('hover'); $(event.currentTarget).removeClass('hover');
}) })
/**
* click on "see all notification"
*/
.on('click', '.notification__print-action', (event) => { .on('click', '.notification__print-action', (event) => {
event.preventDefault(); event.preventDefault();
const $el = $(event.currentTarget); $notificationBoxContainer.hide();
const page = $el.data('page'); $notificationTrigger.removeClass('open');
print_notifications(page); print_notifications(0);
}); });
$(window).bind('resize', function () { $(window).bind('resize', function () {
@@ -62,24 +64,24 @@ const notifyLayout = (services) => {
}; };
const addNotifications = (notificationContent) => { // const addNotifications = (notificationContent) => {
// var box = $('#notification_box'); // // var box = $('#notification_box');
$notificationBoxContainer.empty().append(notificationContent); // $notificationBoxContainer.empty().append(notificationContent);
//
if ($notificationBoxContainer.is(':visible')) { // if ($notificationBoxContainer.is(':visible')) {
setBoxHeight(); // setBoxHeight();
} // }
//
if ($('.notification.unread', $notificationBoxContainer).length > 0) { // if ($('.notification.unread', $notificationBoxContainer).length > 0) {
$('.counter', $notificationTrigger) // $('.counter', $notificationTrigger)
.empty() // .empty()
.append($('.notification.unread', $notificationBoxContainer).length); // .append($('.notification.unread', $notificationBoxContainer).length);
$('.counter', $notificationTrigger).css('visibility', 'visible'); // $('.counter', $notificationTrigger).css('visibility', 'visible');
//
} else { // } else {
$('.notification_trigger .counter').css('visibility', 'hidden').empty(); // $('.notification_trigger .counter').css('visibility', 'hidden').empty();
} // }
}; // };
const setBoxHeight = () => { const setBoxHeight = () => {
@@ -110,20 +112,32 @@ const notifyLayout = (services) => {
} }
}; };
const print_notifications = (page) => { /**
* add 10 notifications into the dlgbox
* display the button "load more" while relevant
*
* @param offset
*/
const print_notifications = (offset) => {
page = parseInt(page, 10); offset = parseInt(offset, 10);
var buttons = {}; var buttons = {};
buttons[localeService.t('fermer')] = function () { buttons[localeService.t('fermer')] = function () {
$notificationDialog.dialog('close'); $notificationDialog.dialog('close');
}; };
// create the dlg div if it does not exists
//
if ($notificationDialog.length === 0) { if ($notificationDialog.length === 0) {
$('body').append('<div id="notifications-dialog" class="loading"></div>'); $('body').append('<div id="notifications-dialog"><div class="content"></div><div class="navigation"></div></div>');
$notificationDialog = $('#notifications-dialog'); $notificationDialog = $('#notifications-dialog');
$notificationsContent = $('.content', $notificationDialog);
$notificationsNavigation = $('.navigation', $notificationDialog);
} }
// open the dlg (even if it is already opened when "load more")
//
$notificationDialog $notificationDialog
.dialog({ .dialog({
title: $('#notification-title').val(), title: $('#notification-title').val(),
@@ -141,20 +155,22 @@ const notifyLayout = (services) => {
close: function (event, ui) { close: function (event, ui) {
$notificationDialog.dialog('destroy').remove(); $notificationDialog.dialog('destroy').remove();
} }
}).dialog('option', 'buttons', buttons).dialog('open').on('click','.notification_next .notification__print-action', function (event) { })
event.preventDefault(); .dialog('option', 'buttons', buttons)
var $el = $(event.currentTarget); .dialog('open');
var page = $el.data('page');
print_notifications(page);
});
// load 10 (more) notifications
//
$notificationDialog.addClass('loading');
$.ajax({ $.ajax({
type: 'GET', type: 'POST',
url: '/user/notifications/', // url: '/user/notifications/',
url: '/session/notifications/',
dataType: 'json', dataType: 'json',
data: { data: {
page: page 'offset': offset,
'limit': 10,
'what': 3, // 3 = read | unread
}, },
error: function (data) { error: function (data) {
$notificationDialog.removeClass('loading'); $notificationDialog.removeClass('loading');
@@ -165,47 +181,55 @@ const notifyLayout = (services) => {
success: function (data) { success: function (data) {
$notificationDialog.removeClass('loading'); $notificationDialog.removeClass('loading');
if (offset === 0) {
if (page === 0) { $notificationsContent.empty();
$notificationDialog.empty();
} else {
$('.notification_next', $notificationDialog).remove();
} }
const notifications = data.notifications.notifications;
let i = 0; let i = 0;
for (i in data.notifications) { for (i in notifications) {
var id = 'notif_date_' + i; const notification = notifications[i];
var date_cont = $('#' + id);
if (date_cont.length === 0) {
$notificationDialog.append('<div id="' + id + '"><div class="notification_title">' + data.notifications[i].display + '</div></div>');
date_cont = $('#' + id);
}
let j = 0; // group notifs by day
for (j in data.notifications[i].notifications) { //
var loc_dat = data.notifications[i].notifications[j]; const date = notification.created_on_day;
var html = '<div style="position:relative;" id="notification_' + loc_dat.id + '" class="notification">' + const id = 'notif_date_' + date;
'<table style="width:100%;" cellspacing="0" cellpadding="0" border="0"><tr><td style="width:25px;">' + let date_cont = $('#' + id, $notificationsContent);
loc_dat.icon + if (date_cont.length === 0) {
'</td><td>' + $notificationsContent.append('<div id="' + id + '"><div class="notification_title">' + notifications[i].created_on + '</div></div>');
'<div style="position:relative;" class="' + loc_dat.classname + '">' + date_cont = $('#' + id, $notificationsContent);
loc_dat.text + ' <span class="time">' + loc_dat.time + '</span></div>' + }
// write notif
let html = '<div style="position:relative;" id="notification_' + notification.id + '" class="notification">' +
'<table style="width:100%;" cellspacing="0" cellpadding="0" border="0"><tr style="border-top: 1px grey solid"><td style="width:25px; vertical-align: top;">' +
'<img src="' + notification.icon + '" style="vertical-align:middle;width:16px;margin:2px;" />' +
'</td><td style="vertical-align: top;">' +
'<div style="position:relative;" class="' + notification.classname + '">' +
notification.text + ' <span class="time">' + notification.time + '</span></div>' +
'</td></tr></table>' + '</td></tr></table>' +
'</div>'; '</div>';
date_cont.append(html); date_cont.append(html);
} }
if (data.notifications.next_page_html) {
$notificationsNavigation
.off('click', '.notification__print-action');
$notificationsNavigation.empty().show().append(data.notifications.next_page_html);
$notificationsNavigation
.on('click', '.notification__print-action', function (event) {
event.preventDefault();
let $el = $(event.currentTarget);
let offset = $el.data('offset');
print_notifications(offset);
});
} }
else {
var next_ln = $.trim(data.next); $notificationsNavigation.empty().hide();
if (next_ln !== '') {
$notificationDialog.append('<div class="notification_next">' + next_ln + '</div>');
} }
} }
}); });
}; };
/* remove in favor of existing /session/ route
const read_notifications = () => { const read_notifications = () => {
var notifications = []; var notifications = [];
@@ -236,9 +260,10 @@ const notifyLayout = (services) => {
$('.notification_trigger .counter').css('visibility', 'hidden').empty(); $('.notification_trigger .counter').css('visibility', 'hidden').empty();
}; };
*/
return { return {
initialize, initialize
addNotifications
}; };
}; };

View File

@@ -2,6 +2,7 @@ import {Observable} from 'rx';
// import {ajax} from 'jquery'; // import {ajax} from 'jquery';
import $ from 'jquery'; import $ from 'jquery';
// module unused now 06-2021
let recordService = (services) => { let recordService = (services) => {
const {configService} = services; const {configService} = services;
const url = configService.get('baseUrl'); const url = configService.get('baseUrl');

View File

@@ -1,7 +1,4 @@
import $ from 'jquery';
import ui from '../ui'; import ui from '../ui';
import notify from '../notify';
import * as appCommons from './../../phraseanet-common';
const user = (services) => { const user = (services) => {
const { configService, localeService, appEvents } = services; const { configService, localeService, appEvents } = services;
@@ -19,53 +16,43 @@ const user = (services) => {
'user.disconnected': onUserDisconnect 'user.disconnected': onUserDisconnect
}); });
const manageSession = (...params) => { // const manageSession = (...params) => {
let [data, showMessages] = params; // let [data, showMessages] = params;
//
// if (typeof (showMessages) === 'undefined') {
// showMessages = false;
// }
//
// if (showMessages) {
// // @todo: to be moved
// if ($.trim(data.message) !== '') {
// if ($('#MESSAGE').length === 0) {
// $('body').append('<div id="#MESSAGE"></div>');
// }
// $('#MESSAGE')
// .empty()
// .append(data.message + '<div style="margin:20px;"><input type="checkbox" class="dialog_remove" />' + localeService.t('hideMessage') + '</div>')
// .attr('title', 'Global Message')
// .dialog({
// autoOpen: false,
// closeOnEscape: true,
// resizable: false,
// draggable: false,
// modal: true,
// close: function () {
// if ($('.dialog_remove:checked', $(this)).length > 0) {
// // @TODO get from module
// appCommons.userModule.setTemporaryPref('message', 0);
// }
// }
// })
// .dialog('open');
// }
// }
// return true;
// };
if (typeof (showMessages) === 'undefined') { return {initialize};
showMessages = false;
}
if (showMessages) {
// @todo: to be moved
if ($.trim(data.message) !== '') {
if ($('#MESSAGE').length === 0) {
$('body').append('<div id="#MESSAGE"></div>');
}
$('#MESSAGE')
.empty()
.append(data.message + '<div style="margin:20px;"><input style="margin-right:10px;" type="checkbox" class="dialog_remove" />' + localeService.t('hideMessage') + '</div>')
.attr('title', 'Global Message')
.dialog({
autoOpen: false,
closeOnEscape: true,
resizable: false,
draggable: false,
modal: true,
close: function () {
if ($('.dialog_remove:checked', $(this)).length > 0) {
// setTemporaryPref
$.ajax({
type: "POST",
url: "/user/preferences/temporary/",
data: {
prop: 'message',
value: 0
},
success: function (data) {
return;
}
});
}
}
})
.dialog('open');
}
}
return true;
};
return {initialize, manageSession};
}; };
export default user; export default user;

View File

@@ -36,7 +36,7 @@
/* eslint-disable no-loop-func*/ /* eslint-disable no-loop-func*/
import $ from 'jquery'; import $ from 'jquery';
import dialog from './dialog';
let cookie = require('js-cookie'); let cookie = require('js-cookie');
const initialize = () => { const initialize = () => {
@@ -56,93 +56,92 @@ const initialize = () => {
}; };
// @deprecated // @deprecated
function manageSession(data, showMessages) { // function manageSession(data, showMessages) {
if (typeof (showMessages) === 'undefined') // if (typeof (showMessages) === 'undefined')
showMessages = false; // showMessages = false;
//
if (data.status === 'disconnected' || data.status === 'session') { // if (data.status === 'disconnected' || data.status === 'session') {
disconnected(); // disconnected();
return false; // return false;
} // }
if (showMessages) { // if (showMessages) {
let box = $('#notification_box'); // let box = $('#notification_box');
box.empty().append(data.notifications); // box.empty().append(data.notifications);
//
if (box.is(':visible')) // if (box.is(':visible'))
fix_notification_height(); // fix_notification_height();
//
if ($('.notification.unread', box).length > 0) { // if ($('.notification.unread', box).length > 0) {
let trigger = $('#notification_trigger'); // let trigger = $('#notification_trigger');
$('.counter', trigger) // $('.counter', trigger)
.empty() // .empty()
.append($('.notification.unread', box).length); // .append($('.notification.unread', box).length);
$('.counter', trigger).css('visibility', 'visible'); // $('.counter', trigger).css('visibility', 'visible');
//
} // }
else // else
$('#notification_trigger .counter').css('visibility', 'hidden').empty(); // $('#notification_trigger .counter').css('visibility', 'hidden').empty();
//
if (data.changed.length > 0) { // if (data.changed.length > 0) {
let current_open = $('.SSTT.ui-state-active'); // let current_open = $('.SSTT.ui-state-active');
let current_sstt = current_open.length > 0 ? current_open.attr('id').split('_').pop() : false; // let current_sstt = current_open.length > 0 ? current_open.attr('id').split('_').pop() : false;
//
let main_open = false; // let main_open = false;
for (let i = 0; i !== data.changed.length; i++) { // for (let i = 0; i !== data.changed.length; i++) {
let sstt = $('#SSTT_' + data.changed[i]); // let sstt = $('#SSTT_' + data.changed[i]);
if (sstt.size() === 0) { // if (sstt.size() === 0) {
if (main_open === false) { // if (main_open === false) {
$('#baskets .bloc').animate({'top': 30}, function () { // $('#baskets .bloc').animate({'top': 30}, function () {
$('#baskets .alert_datas_changed:first').show(); // $('#baskets .alert_datas_changed:first').show();
}); // });
main_open = true; // main_open = true;
} // }
} // }
else { // else {
if (!sstt.hasClass('active')) // if (!sstt.hasClass('active'))
sstt.addClass('unread'); // sstt.addClass('unread');
else { // else {
$('.alert_datas_changed', $('#SSTT_content_' + data.changed[i])).show(); // $('.alert_datas_changed', $('#SSTT_content_' + data.changed[i])).show();
} // }
} // }
} // }
} // }
if ($.trim(data.message) !== '') { // if ($.trim(data.message) !== '') {
if ($('#MESSAGE').length === 0) // if ($('#MESSAGE').length === 0)
$('body').append('<div id="#MESSAGE"></div>'); // $('body').append('<div id="#MESSAGE"></div>');
$('#MESSAGE') // $('#MESSAGE')
.empty() // .empty()
.append('<div style="margin:30px 10px;"><h4><b>' + data.message + '</b></h4></div><div style="margin:20px 0px 10px;"><label class="checkbox"><input type="checkbox" class="dialog_remove" />' + language.hideMessage + '</label></div>') // .append('<div style="margin:30px 10px;"><h4><b>' + data.message + '</b></h4></div><div style="margin:20px 0px 10px;"><label class="checkbox"><input type="checkbox" class="dialog_remove" />' + language.hideMessage + '</label></div>')
.attr('title', 'Global Message') // .attr('title', 'Global Message')
.dialog({ // .dialog({
autoOpen: false, // autoOpen: false,
closeOnEscape: true, // closeOnEscape: true,
resizable: false, // resizable: false,
draggable: false, // draggable: false,
modal: true, // modal: true,
close: function () { // close: function () {
if ($('.dialog_remove:checked', $(this)).length > 0) { // if ($('.dialog_remove:checked', $(this)).length > 0) {
// setTemporaryPref // // setTemporaryPref
$.ajax({ // $.ajax({
type: 'POST', // type: 'POST',
url: '/user/preferences/temporary/', // url: '/user/preferences/temporary/',
data: { // data: {
prop: 'message', // prop: 'message',
value: 0 // value: 0
}, // },
success: function (data) { // success: function (data) {
return; // return;
} // }
}); // });
} // }
} // }
}) // })
.dialog('open'); // .dialog('open');
} // }
} // }
return true; // return true;
} // }
export default { export default {
initialize, initialize
manageSession
}; };

View File

@@ -1,15 +1,11 @@
import $ from 'jquery'; import $ from 'jquery';
const humane = require('humane-js');
require('imports-loader?define=>false&exports=>false!./../components/utils/jquery-plugins/colorAnimation');
import * as AppCommons from './../phraseanet-common'; import * as AppCommons from './../phraseanet-common';
import publication from '../components/publication'; import publication from '../components/publication';
import workzone from '../components/ui/workzone'; import workzone from '../components/ui/workzone';
import notify from '../components/notify/index'; import notifyLayout from '../components/notify/notifyLayout';
import Locale from '../components/locale'; import LocaleService from '../components/locale';
import ui from '../components/ui'; import ui from '../components/ui';
import ConfigService from './../components/core/configService'; import ConfigService from './../components/core/configService';
import LocaleService from '../components/locale';
import i18next from 'i18next';
import defaultConfig from './config'; import defaultConfig from './config';
import Emitter from '../components/core/emitter'; import Emitter from '../components/core/emitter';
import user from '../components/user'; import user from '../components/user';
@@ -18,6 +14,10 @@ import search from '../components/search';
import utils from './../components/utils/utils'; import utils from './../components/utils/utils';
import dialog from './../phraseanet-common/components/dialog'; import dialog from './../phraseanet-common/components/dialog';
import merge from 'lodash.merge'; import merge from 'lodash.merge';
const humane = require('humane-js');
require('imports-loader?define=>false&exports=>false!./../components/utils/jquery-plugins/colorAnimation');
class Bootstrap { class Bootstrap {
app; app;
@@ -69,27 +69,28 @@ class Bootstrap {
const userSession = user(this.appServices); const userSession = user(this.appServices);
let appProdNotification = { // let appProdNotification = {
url: this.configService.get('notify.url'), // url: this.configService.get('notify.url'),
moduleId: this.configService.get('notify.moduleId'), // moduleId: this.configService.get('notify.moduleId'),
userId: this.configService.get('notify.userId') // userId: this.configService.get('notify.userId')
}; // };
notifyLayout(this.appServices).initialize();
/** /**
* Initialize notifier * Poll just in menu_bar
* @type {{bindEvents, createNotifier, isValid, poll}}
*/ */
const notifier = notify(this.appServices); // const notifier = notify(this.appServices);
notifier.initialize(); // notifier.initialize();
//
// create a new notification poll: // // create a new notification poll:
appProdNotification = notifier.createNotifier(appProdNotification); // appProdNotification = notifier.createNotifier(appProdNotification);
//
if (notifier.isValid(appProdNotification)) { // if (notifier.isValid(appProdNotification)) {
notifier.poll(appProdNotification); // notifier.poll(appProdNotification);
} else { // } else {
throw new Error('implementation error: failed to configure new notifier'); // throw new Error('implementation error: failed to configure new notifier');
} // }
// @TODO remove global variables // @TODO remove global variables
// register some global variables, // register some global variables,

View File

@@ -11,10 +11,10 @@ namespace Alchemy\Phrasea\Controller\Root;
use Alchemy\Phrasea\Application\Helper\EntityManagerAware; use Alchemy\Phrasea\Application\Helper\EntityManagerAware;
use Alchemy\Phrasea\Controller\Controller; use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Model\Entities\SessionModule;
use Alchemy\Phrasea\Model\Repositories\BasketRepository; use Alchemy\Phrasea\Model\Repositories\BasketRepository;
use Alchemy\Phrasea\Model\Repositories\SessionRepository; use Alchemy\Phrasea\Model\Repositories\SessionRepository;
use Alchemy\Phrasea\Utilities\Stopwatch; use Alchemy\Phrasea\Utilities\Stopwatch;
use eventsmanager_broker;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -42,10 +42,14 @@ class SessionController extends Controller
'status' => 'unknown', 'status' => 'unknown',
'message' => '', 'message' => '',
'notifications' => false, 'notifications' => false,
'changed' => [] 'notifications_html' => false,
'unread_basket_ids' => []
]; ];
$authenticator = $this->getAuthenticator(); $authenticator = $this->getAuthenticator();
// every request is catched by SessionManagerSubscriber, which handles disconnect
// so this code is probably useless
/*
if ($authenticator->isAuthenticated()) { if ($authenticator->isAuthenticated()) {
$usr_id = $authenticator->getUser()->getId(); $usr_id = $authenticator->getUser()->getId();
if ($usr_id != $request->request->get('usr')) { // I logged with another user if ($usr_id != $request->request->get('usr')) { // I logged with another user
@@ -58,29 +62,39 @@ class SessionController extends Controller
return $this->app->json($ret); return $this->app->json($ret);
} }
*/
try { try {
$this->getApplicationBox()->get_connection(); $this->getApplicationBox()->get_connection();
} catch (\Exception $e) { }
catch (\Exception $e) {
return $this->app->json($ret); return $this->app->json($ret);
} }
// module id is only used to track apps, its done in SessioManagerSubscriber (parsing url)
/*
if (1 > $moduleId = (int) $request->request->get('module')) { if (1 > $moduleId = (int) $request->request->get('module')) {
$ret['message'] = 'Missing or Invalid `module` parameter'; $ret['message'] = 'Missing or Invalid `module` parameter';
return $this->app->json($ret); return $this->app->json($ret);
} }
*/
$ret['status'] = 'ok'; $ret['status'] = 'ok';
$stopwatch->lap("start"); $stopwatch->lap("start");
$notifs = $this->getEventsManager()->get_notifications($stopwatch); $offset = (int)$request->get('offset', 0);
$limit = (int)$request->get('limit', 10);
$what = (int)$request->get('what', eventsmanager_broker::UNREAD | eventsmanager_broker::READ);
$notifications = $this->getEventsManager()->get_notifications($offset, $limit, $what, $stopwatch);
$stopwatch->lap("get_notifications done"); $stopwatch->lap("get_notifications done");
$ret['notifications'] = $this->render('prod/notifications.html.twig', [ $ret['notifications'] = $notifications;
'notifications' => $notifs $ret['notifications_html'] = $this->render('prod/notifications.html.twig', [
'notifications' => $notifications['notifications']
]); ]);
$stopwatch->lap("render done"); $stopwatch->lap("render done");
@@ -90,7 +104,7 @@ class SessionController extends Controller
$stopwatch->lap("baskets::findUnreadActiveByUser done"); $stopwatch->lap("baskets::findUnreadActiveByUser done");
foreach ($baskets as $basket) { foreach ($baskets as $basket) {
$ret['changed'][] = $basket->getId(); $ret['unread_basket_ids'][] = $basket->getId();
} }
if (in_array($this->getSession()->get('phraseanet.message'), ['1', null])) { if (in_array($this->getSession()->get('phraseanet.message'), ['1', null])) {
@@ -121,96 +135,6 @@ class SessionController extends Controller
return $response; return $response;
} }
/**
* @param Request $request
* @return JsonResponse
* @throws \Exception in case "new \DateTime()" fails ?
*/
public function updateSession(Request $request)
{
if (!$request->isXmlHttpRequest()) {
$this->app->abort(400);
}
$ret = [
'status' => 'unknown',
'message' => '',
'notifications' => false,
'changed' => []
];
$authenticator = $this->getAuthenticator();
if ($authenticator->isAuthenticated()) {
$usr_id = $authenticator->getUser()->getId();
if ($usr_id != $request->request->get('usr')) { // I logged with another user
$ret['status'] = 'disconnected';
return $this->app->json($ret);
}
}
else {
$ret['status'] = 'disconnected';
return $this->app->json($ret);
}
try {
$this->getApplicationBox()->get_connection();
}
catch (\Exception $e) {
return $this->app->json($ret);
}
if (1 > $moduleId = (int) $request->request->get('module')) {
$ret['message'] = 'Missing or Invalid `module` parameter';
return $this->app->json($ret);
}
/** @var \Alchemy\Phrasea\Model\Entities\Session $session */
$session = $this->getSessionRepository()->find($this->getSession()->get('session_id'));
$session->setUpdated(new \DateTime());
$manager = $this->getEntityManager();
if (!$session->hasModuleId($moduleId)) {
$module = new SessionModule();
$module->setModuleId($moduleId);
$module->setSession($session);
$manager->persist($module);
}
else {
$manager->persist($session->getModuleById($moduleId)->setUpdated($now));
}
$manager->persist($session);
$manager->flush();
$ret['status'] = 'ok';
$ret['notifications'] = $this->render('prod/notifications.html.twig', [
'notifications' => $this->getEventsManager()->get_notifications()
]);
$baskets = $this->getBasketRepository()->findUnreadActiveByUser($authenticator->getUser());
foreach ($baskets as $basket) {
$ret['changed'][] = $basket->getId();
}
if (in_array($this->getSession()->get('phraseanet.message'), ['1', null])) {
$conf = $this->getConf();
if ($conf->get(['main', 'maintenance'])) {
$ret['message'] .= $this->app->trans('The application is going down for maintenance, please logout.');
}
if ($conf->get(['registry', 'maintenance', 'enabled'])) {
$ret['message'] .= strip_tags($conf->get(['registry', 'maintenance', 'message']));
}
}
return $this->app->json($ret);
}
/** /**
* Deletes identified session * Deletes identified session
* *

View File

@@ -21,6 +21,7 @@ class UserNotificationController extends Controller
* @param Request $request * @param Request $request
* @return JsonResponse * @return JsonResponse
*/ */
/* remove in favor of existing /session/ route
public function readNotifications(Request $request) public function readNotifications(Request $request)
{ {
if (!$request->isXmlHttpRequest()) { if (!$request->isXmlHttpRequest()) {
@@ -38,6 +39,7 @@ class UserNotificationController extends Controller
return $this->app->json(['success' => false, 'message' => $e->getMessage()]); return $this->app->json(['success' => false, 'message' => $e->getMessage()]);
} }
} }
*/
/** /**
* Get all notifications * Get all notifications
@@ -45,6 +47,7 @@ class UserNotificationController extends Controller
* @param Request $request * @param Request $request
* @return JsonResponse * @return JsonResponse
*/ */
/* remove in favor of existing /session/ route
public function listNotifications(Request $request) public function listNotifications(Request $request)
{ {
if (!$request->isXmlHttpRequest()) { if (!$request->isXmlHttpRequest()) {
@@ -55,12 +58,15 @@ class UserNotificationController extends Controller
return $this->app->json($this->getEventsManager()->get_notifications_as_array(($page < 0 ? 0 : $page))); return $this->app->json($this->getEventsManager()->get_notifications_as_array(($page < 0 ? 0 : $page)));
} }
*/
/** /**
* @return \eventsmanager_broker * @return \eventsmanager_broker
*/ */
/* remove in favor of existing /session/ route
private function getEventsManager() private function getEventsManager()
{ {
return $this->app['events-manager']; return $this->app['events-manager'];
} }
*/
} }

View File

@@ -13,9 +13,8 @@ namespace Alchemy\Phrasea\ControllerProvider\Admin;
use Alchemy\Phrasea\Application as PhraseaApplication; use Alchemy\Phrasea\Application as PhraseaApplication;
use Alchemy\Phrasea\Controller\Admin\DataboxController; use Alchemy\Phrasea\Controller\Admin\DataboxController;
use Alchemy\Phrasea\Core\LazyLocator;
use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait; use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait;
use Alchemy\Phrasea\Security\Firewall; use Alchemy\Phrasea\Core\LazyLocator;
use Silex\Application; use Silex\Application;
use Silex\ControllerProviderInterface; use Silex\ControllerProviderInterface;
use Silex\ServiceProviderInterface; use Silex\ServiceProviderInterface;
@@ -89,6 +88,7 @@ class Databox implements ControllerProviderInterface, ServiceProviderInterface
->before([$this, 'requireChangeSbasStructureRight']) ->before([$this, 'requireChangeSbasStructureRight'])
->bind('admin_database_submit_cgus'); ->bind('admin_database_submit_cgus');
// polled by admin/databox to display indexation progress bar
$controllers->get('/{databox_id}/informations/documents/', 'controller.admin.databox:progressBarInfos') $controllers->get('/{databox_id}/informations/documents/', 'controller.admin.databox:progressBarInfos')
->before([$this, 'requireManageRightOnSbas']) ->before([$this, 'requireManageRightOnSbas'])
->bind('admin_database_display_document_information'); ->bind('admin_database_display_document_information');

View File

@@ -12,9 +12,9 @@
namespace Alchemy\Phrasea\ControllerProvider\Root; namespace Alchemy\Phrasea\ControllerProvider\Root;
use Alchemy\Phrasea\Application as PhraseaApplication; use Alchemy\Phrasea\Application as PhraseaApplication;
use Alchemy\Phrasea\Core\LazyLocator;
use Alchemy\Phrasea\Controller\Root\SessionController; use Alchemy\Phrasea\Controller\Root\SessionController;
use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait; use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait;
use Alchemy\Phrasea\Core\LazyLocator;
use Silex\Application; use Silex\Application;
use Silex\ControllerProviderInterface; use Silex\ControllerProviderInterface;
use Silex\ServiceProviderInterface; use Silex\ServiceProviderInterface;
@@ -41,12 +41,12 @@ class Session implements ControllerProviderInterface, ServiceProviderInterface
{ {
$controllers = $this->createCollection($app); $controllers = $this->createCollection($app);
$controllers->post('/update/', 'controller.session:updateSession') /** @uses SessionController::getNotifications() */
->bind('update_session');
$controllers->post('/notifications/', 'controller.session:getNotifications') $controllers->post('/notifications/', 'controller.session:getNotifications')
->bind('list_notifications'); ->bind('list_notifications');
/** @uses SessionController::deleteSession() */
// used in admin/connected_users to kill a session
$controller = $controllers->post('/delete/{id}', 'controller.session:deleteSession') $controller = $controllers->post('/delete/{id}', 'controller.session:deleteSession')
->bind('delete_session'); ->bind('delete_session');

View File

@@ -11,8 +11,6 @@
namespace Alchemy\Phrasea\ControllerProvider\User; namespace Alchemy\Phrasea\ControllerProvider\User;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Alchemy\Phrasea\Controller\User\UserNotificationController;
use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait; use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait;
use Silex\Application; use Silex\Application;
use Silex\ControllerProviderInterface; use Silex\ControllerProviderInterface;
@@ -24,9 +22,12 @@ class Notifications implements ControllerProviderInterface, ServiceProviderInter
public function register(Application $app) public function register(Application $app)
{ {
/* remove in favor of existing /session/ route
*
$app['controller.user.notifications'] = $app->share(function (PhraseaApplication $app) { $app['controller.user.notifications'] = $app->share(function (PhraseaApplication $app) {
return (new UserNotificationController($app)); return (new UserNotificationController($app));
}); });
*/
} }
public function boot(Application $app) public function boot(Application $app)
@@ -46,11 +47,16 @@ class Notifications implements ControllerProviderInterface, ServiceProviderInter
$firewall->requireNotGuest(); $firewall->requireNotGuest();
}); });
/* remove in favor of existing /session/ route
*
/** @uses UserNotificationController::listNotifications * /
$controllers->get('/', 'controller.user.notifications:listNotifications') $controllers->get('/', 'controller.user.notifications:listNotifications')
->bind('get_notifications'); ->bind('get_notifications');
/** @uses UserNotificationController::readNotifications() * /
$controllers->post('/read/', 'controller.user.notifications:readNotifications') $controllers->post('/read/', 'controller.user.notifications:readNotifications')
->bind('set_notifications_readed'); ->bind('set_notifications_readed');
*/
return $controllers; return $controllers;
} }

View File

@@ -13,13 +13,12 @@ namespace Alchemy\Phrasea\Core\Event\Subscriber;
use Alchemy\Phrasea\Application; use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Model\Entities\Session; use Alchemy\Phrasea\Model\Entities\Session;
use Alchemy\Phrasea\Model\Entities\SessionModule; use Alchemy\Phrasea\Model\Entities\SessionModule;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class SessionManagerSubscriber implements EventSubscriberInterface class SessionManagerSubscriber implements EventSubscriberInterface
{ {
@@ -43,56 +42,52 @@ class SessionManagerSubscriber implements EventSubscriberInterface
{ {
return [ return [
KernelEvents::REQUEST => [ KernelEvents::REQUEST => [
['initSession', Application::EARLY_EVENT], /** @uses SessionManagerSubscriber::checkSessionActivity */
['checkSessionActivity', Application::LATE_EVENT] ['checkSessionActivity', Application::LATE_EVENT]
] ]
]; ];
} }
public function initSession(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$request = $event->getRequest();
if ($this->isFlashUploadRequest($request) && null !== $sessionId = $request->request->get('php_session_id')) {
$request->cookies->set($this->app['session']->getName(), $sessionId);
}
}
/** /**
* log real human activity on application, to keep session alive * log real human activity on application, to keep session alive
*
* to "auto-disconnect" when idle duration is passed, we use the "poll" requests.
* nb : the route "/sessions/notifications" is not considered as comming from a "module" (prod, admin, ...)
* so it will not update session
*
* @param GetResponseEvent $event * @param GetResponseEvent $event
*/ */
public function checkSessionActivity(GetResponseEvent $event) public function checkSessionActivity(GetResponseEvent $event)
{ {
$request = $event->getRequest(); $request = $event->getRequest();
// ignore routes that comes from api (?) : todo : check if usefull since "api" is not a "module"
// todo : "LOG" ???
if ($request->request->has('oauth_token') if ($request->request->has('oauth_token')
|| $request->query->has('oauth_token') || $request->query->has('oauth_token')
|| $request->query->has('LOG') || $request->query->has('LOG')
|| null === $moduleId = $this->getModuleId($request->getPathInfo())
) { ) {
return; return;
} }
if ($this->isAdminJsPolledRoute($moduleId, $request)) { $moduleId = $this->getModuleId($request->getPathInfo());
return;
}
if ($moduleId === self::$modulesIds['prod'] && $this->isFlashUploadRequest($request)) {
return;
}
// if we are already disconnected (ex. from another window), quit immediately // if we are already disconnected (ex. from another window), quit immediately
//
if (!($this->app->getAuthenticator()->isAuthenticated())) { if (!($this->app->getAuthenticator()->isAuthenticated())) {
$this->setDisconnectResponse($event); $this->setDisconnectResponse($event);
return; return;
} }
// we must still ignore some "polling" (js) routes
//
if ($this->isJsPollingRoute($moduleId, $request)) {
return;
}
// ANY route can disconnect the user if idle duration is over
//
/** @var Session $session */ /** @var Session $session */
$session = $this->app['repo.sessions']->find($this->app['session']->get('session_id')); $session = $this->app['repo.sessions']->find($this->app['session']->get('session_id'));
@@ -112,6 +107,14 @@ class SessionManagerSubscriber implements EventSubscriberInterface
return; return;
} }
// only routes from "modules" (prod, admin, ...) are considered as "user activity"
//
if(is_null($moduleId)) {
return;
}
// here the route is considered as "user activity" : update session
//
$entityManager = $this->app['orm.em']; $entityManager = $this->app['orm.em'];
$module = $this->addOrUpdateSessionModule($session, $moduleId, $now); $module = $this->addOrUpdateSessionModule($session, $moduleId, $now);
@@ -121,11 +124,6 @@ class SessionManagerSubscriber implements EventSubscriberInterface
$entityManager->flush(); $entityManager->flush();
} }
private function isFlashUploadRequest(Request $request)
{
return false !== stripos($request->server->get('HTTP_USER_AGENT'), 'flash') && $request->getRequestUri() === '/prod/upload/';
}
/** /**
* @param GetResponseEvent $event * @param GetResponseEvent $event
*/ */
@@ -133,10 +131,11 @@ class SessionManagerSubscriber implements EventSubscriberInterface
{ {
$request = $event->getRequest(); $request = $event->getRequest();
if($this->getModuleName($request->getPathInfo()) !== 'login') { // prevent infinite redirections
$response = $request->isXmlHttpRequest() ? $this->getXmlHttpResponse() : $this->getRedirectResponse($request); $response = $request->isXmlHttpRequest() ? $this->getXmlHttpResponse() : $this->getRedirectResponse($request);
$event->setResponse($response); $event->setResponse($response);
} }
}
/** /**
* @return Response * @return Response
@@ -163,7 +162,7 @@ class SessionManagerSubscriber implements EventSubscriberInterface
* @param string $pathInfo * @param string $pathInfo
* @return int|null * @return int|null
*/ */
private function getModuleId($pathInfo) private function getModuleName($pathInfo)
{ {
$parts = array_filter(explode('/', $pathInfo)); $parts = array_filter(explode('/', $pathInfo));
@@ -171,7 +170,16 @@ class SessionManagerSubscriber implements EventSubscriberInterface
return null; return null;
} }
$moduleName = strtolower($parts[1]); return strtolower($parts[1]);
}
/**
* @param string $pathInfo
* @return int|null
*/
private function getModuleId($pathInfo)
{
$moduleName = $this->getModuleName($pathInfo);
if (!isset(self::$modulesIds[$moduleName])) { if (!isset(self::$modulesIds[$moduleName])) {
return null; return null;
@@ -181,23 +189,39 @@ class SessionManagerSubscriber implements EventSubscriberInterface
} }
/** /**
* returns true is the route match a "polling" route (databox progressionbar, task manager, notifications, ...)
* polling routes (sent every n seconds with no user action) must not update the session
*
* the request should contain a "update-session=0" header, but for now we still test hardcoded routes
*
* @param int $moduleId * @param int $moduleId
* @param Request $request * @param Request $request
* @return bool * @return bool
*/ */
private function isAdminJsPolledRoute($moduleId, Request $request) private function isJsPollingRoute($moduleId, Request $request)
{ {
if ($moduleId !== self::$modulesIds['admin']) { if($request->headers->get('update-session', '1') === '0') {
return false; return true;
} }
$pathInfo = $request->getPathInfo(); $pathInfo = $request->getPathInfo();
// admin/task managers poll tasks
if ($pathInfo === '/admin/task-manager/tasks/' && $request->getContentType() === 'json') { if ($pathInfo === '/admin/task-manager/tasks/' && $request->getContentType() === 'json') {
return true; return true;
} }
return preg_match('#^/admin/databox/\d+/informations/documents/#', $pathInfo) === 1; // admin/databox poll to update the indexation progress bar (header "update-session=0") sent
if(preg_match('#^/admin/databox/\d+/informations/documents/#', $pathInfo)) {
return true;
}
// admin/databox poll to update the indexation progress bar
if(preg_match('#^/.*/notifications/#', $pathInfo)) {
return true;
}
return false;
} }
/** /**

View File

@@ -96,15 +96,29 @@ class eventsmanager_broker
return true; return true;
} }
/*
public function get_notifications_as_array($page = 0) public function get_notifications_as_array($page = 0)
{ {
$r = $this->get_notifications($page * 10, 10);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !!!!!!!!!!!!!!!!!!!!!!!!! FAKE USER FOR TESTING !!!!!!!!!!!!!!!!!!!!!!!!
// $usr_id = $this->app->getAuthenticatedUser()->getId();
$usr_id = 15826;
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
$total = 0; $total = 0;
$sql = 'SELECT count(id) as total, sum(unread) as unread $sql = 'SELECT count(id) as total, sum(unread) as unread
FROM notifications WHERE usr_id = :usr_id'; FROM notifications WHERE usr_id = :usr_id';
$stmt = $this->app->getApplicationBox()->get_connection()->prepare($sql); $stmt = $this->app->getApplicationBox()->get_connection()->prepare($sql);
$stmt->execute([':usr_id' => $this->app->getAuthenticatedUser()->getId()]); $stmt->execute([':usr_id' => $usr_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt->closeCursor(); $stmt->closeCursor();
@@ -122,7 +136,7 @@ class eventsmanager_broker
$data = ['notifications' => [], 'next' => '']; $data = ['notifications' => [], 'next' => ''];
$stmt = $this->app->getApplicationBox()->get_connection()->prepare($sql); $stmt = $this->app->getApplicationBox()->get_connection()->prepare($sql);
$stmt->execute([':usr_id' => $this->app->getAuthenticatedUser()->getId()]); $stmt->execute([':usr_id' => $usr_id]);
$rs = $stmt->fetchAll(PDO::FETCH_ASSOC); $rs = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt->closeCursor(); $stmt->closeCursor();
@@ -134,7 +148,9 @@ class eventsmanager_broker
continue; continue;
} }
$content = $this->pool_classes[$type]->datas($json, $row['unread']); /** @var eventsmanager_notifyAbstract $obj * /
$obj = $this->pool_classes[$type];
$content = $obj->datas($json, $row['unread']);
if ( ! isset($this->pool_classes[$type]) || count($content) === 0) { if ( ! isset($this->pool_classes[$type]) || count($content) === 0) {
$sql = 'DELETE FROM notifications WHERE id = :id'; $sql = 'DELETE FROM notifications WHERE id = :id';
@@ -169,7 +185,8 @@ class eventsmanager_broker
return $data; return $data;
} }
*/
/*
public function get_unread_notifications_number() public function get_unread_notifications_number()
{ {
$total = 0; $total = 0;
@@ -188,92 +205,152 @@ class eventsmanager_broker
return $total; return $total;
} }
*/
const READ = 1;
const UNREAD = 2;
public function get_notifications(\Alchemy\Phrasea\Utilities\Stopwatch $stopwatch = null) public function get_notifications(int $offset=0, int $limit=10, $readFilter = self::READ | self::UNREAD, \Alchemy\Phrasea\Utilities\Stopwatch $stopwatch = null)
{ {
$unread = 0; if($stopwatch) $stopwatch->lap("broker start");
$sql = 'SELECT count(id) as total, sum(unread) as unread
FROM notifications WHERE usr_id = :usr_id';
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !!!!!!!!!!!!!!!!!!!!!!!!! FAKE USER FOR TESTING !!!!!!!!!!!!!!!!!!!!!!!! // !!!!!!!!!!!!!!!!!!!!!!!!! FAKE USER FOR TESTING !!!!!!!!!!!!!!!!!!!!!!!!
// $usr_id = $this->app->getAuthenticatedUser()->getId(); $usr_id = $this->app->getAuthenticatedUser()->getId();
$usr_id = 29882; // $usr_id = 29882;
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
if($stopwatch) $stopwatch->lap("broker start");
// delete old already read notifs (nb: we do this for everybody - not only the current user -)
// todo: for now we use "created_on" since there is no timestamp set when reading.
//
$sql = "DELETE FROM `notifications` WHERE `unread`=0 AND TIMESTAMPDIFF(HOUR, `created_on`, NOW()) > 10";
$this->app->getApplicationBox()->get_connection()->exec($sql);
// get count of unread notifications (to be displayed on navbar)
//
$total = 0;
$unread = 0;
$sql = 'SELECT COUNT(`id`) AS `total`, SUM(`unread`) AS `unread` FROM `notifications` WHERE `usr_id` = :usr_id';
$stmt = $this->app->getApplicationBox()->get_connection()->prepare($sql); $stmt = $this->app->getApplicationBox()->get_connection()->prepare($sql);
$stmt->execute([':usr_id' => $usr_id]); $stmt->execute([':usr_id' => $usr_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC); if( ($row = $stmt->fetch(PDO::FETCH_ASSOC)) ) {
$total = (int)$row['total'];
$unread = (int)$row['unread'];
}
$stmt->closeCursor(); $stmt->closeCursor();
if($stopwatch) $stopwatch->lap("sql 1"); if($stopwatch) $stopwatch->lap("sql count unread");
if ($row) { $notifications = [];
$unread = $row['unread'];
// fetch notifications
//
$sql = "SELECT * FROM `notifications` WHERE `usr_id` = :usr_id";
switch ($readFilter) {
case self::READ:
$sql .= " AND `unread`=0";
$total -= $unread; // fix total to match the filter
break;
case self::UNREAD:
$sql .= " AND `unread`=1";
$total = $unread; // fix total to match the filter
break;
default:
// any other case : fetch both ; no need to fix total
break;
} }
$sql .= " ORDER BY created_on DESC LIMIT " . $offset . ", " . $limit;
if ($unread < 3) {
$sql = 'SELECT * FROM notifications
WHERE usr_id = :usr_id ORDER BY created_on DESC LIMIT 0,4';
} else {
$sql = 'SELECT * FROM notifications
WHERE usr_id = :usr_id AND unread="1" ORDER BY created_on DESC';
}
$ret = [];
$stmt = $this->app->getApplicationBox()->get_connection()->prepare($sql); $stmt = $this->app->getApplicationBox()->get_connection()->prepare($sql);
$stmt->execute([':usr_id' => $usr_id]); $stmt->execute([':usr_id' => $usr_id]); // , ':offset' => $offset, ':limit' => $limit]);
// $rs = $stmt->fetchAll(PDO::FETCH_ASSOC); $rs = $stmt->fetchAll(PDO::FETCH_ASSOC);
//$stmt->closeCursor(); $stmt->closeCursor();
if($stopwatch) $stopwatch->lap("sql 2"); if($stopwatch) $stopwatch->lap("sql 2");
// foreach ($rs as $row) { $bad_ids = [];
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { // nb : we asked for a "page" of notifs (limit), but since some notifications may be ignored (bad type, bad json, ...)
// the result array may contain less than expected.
foreach ($rs as $row) {
$type = 'eventsmanager_' . $row['type']; $type = 'eventsmanager_' . $row['type'];
if ( ! isset($this->pool_classes[$type])) { if ( ! isset($this->pool_classes[$type])) {
$bad_ids[] = $row['id'];
continue; continue;
} }
$data = @json_decode($row['datas'], true); $data = @json_decode($row['datas'], true);
if (json_last_error() !== JSON_ERROR_NONE) { if (json_last_error() !== JSON_ERROR_NONE) {
$bad_ids[] = $row['id'];
continue; continue;
} }
$datas = $this->pool_classes[$type]->datas($data, $row['unread']); /** @var eventsmanager_notifyAbstract $obj */
$obj = $this->pool_classes[$type];
$datas = $obj->datas($data, $row['unread']);
// $datas = [
// 'text' => "blabla"
// 'class' => "" | "relaod_baskets"
// ]
// if ( ! isset($this->pool_classes[$type]) || count($datas) === 0) { if (count($datas) === 0) {
// $sql = 'DELETE FROM notifications WHERE id = :id'; $bad_ids[] = $row['id'];
// $stmt = $this->app->getApplicationBox()->get_connection()->prepare($sql); continue;
// $stmt->execute([':id' => $row['id']]); }
// $stmt->closeCursor();
// continue;
// }
$ret[] = array_merge( // add infos to the notification and add to list
$datas //
, [ $created_on = new DateTime($row['created_on']);
'created_on' => $this->app['date-formatter']->getPrettyString(new DateTime($row['created_on'])) $notifications[] = array_merge(
, 'icon' => $this->pool_classes[$type]->icon_url() $datas,
, 'id' => $row['id'] [
, 'unread' => $row['unread'] 'created_on_day' => $created_on->format('Ymd'),
'created_on' => $this->app['date-formatter']->getPrettyString($created_on),
'time' => $this->app['date-formatter']->getTime($created_on),
//, 'icon' => '<img src="' . $this->pool_classes[$type]->icon_url() . '" style="vertical-align:middle;width:16px;margin:2px;" />'
'icon' => $this->pool_classes[$type]->icon_url(),
'id' => $row['id'],
'unread' => $row['unread']
] ]
); );
} }
$stmt->closeCursor(); $stmt->closeCursor();
if($stopwatch) $stopwatch->lap("broker ret"); if($stopwatch) $stopwatch->lap("fetch");
if(!empty($bad_ids)) {
// delete broken notifs
$sql = 'DELETE FROM `notifications` WHERE `id` IN (' . join(',', $bad_ids) . ')';
$stmt = $this->app->getApplicationBox()->get_connection()->exec($sql);
}
$next_offset = $offset+$limit;
$ret = [
'unread_count' => $unread,
'offset' => $offset,
'limit' => $limit,
// 'prev_offset' => $offset === 0 ? null : max(0, $offset-$limit),
'next_offset' => $next_offset < $total ? $next_offset : null,
'next_page_html' => $next_offset < $total ? '<a href="#" class="notification__print-action" data-offset="' . $next_offset . '">' . $this->app->trans('charger d\'avantages de notifications') . '</a>' : null,
'notifications' => $notifications
];
return $ret; return $ret;
} }
/**
* mark a notification as read
* todo : add a "read_on" datetime so we can delete read notifs after N days. For now we use "created_on"
*
* @param array $notifications
* @param $usr_id
* @return $this|false
*/
public function read(Array $notifications, $usr_id) public function read(Array $notifications, $usr_id)
{ {
if (count($notifications) == 0) { if (count($notifications) == 0) {

View File

@@ -1,3 +1,4 @@
// import notifyLayout from "../../../../../Phraseanet-production-client/src/components/notify/notifyLayout";
var p4 = p4 || {}; var p4 = p4 || {};
var datepickerLang = []; var datepickerLang = [];
@@ -32,7 +33,6 @@ var commonModule = (function ($, p4) {
$(this).removeClass('context-menu-item-hover'); $(this).removeClass('context-menu-item-hover');
}); });
$('body').on('click', '.infoDialog', function (event) { $('body').on('click', '.infoDialog', function (event) {
infoDialog($(this)); infoDialog($(this));
}); });
@@ -42,11 +42,13 @@ var commonModule = (function ($, p4) {
function showOverlay(n, appendto, callback, zIndex) { function showOverlay(n, appendto, callback, zIndex) {
var div = "OVERLAY"; var div = "OVERLAY";
if (typeof(n) != "undefined") if (typeof(n) != "undefined") {
div += n; div += n;
}
if ($('#' + div).length === 0) { if ($('#' + div).length === 0) {
if (typeof(appendto) == 'undefined') if (typeof(appendto) == 'undefined') {
appendto = 'body'; appendto = 'body';
}
$(appendto).append('<div id="' + div + '" style="display:none;">&nbsp;</div>'); $(appendto).append('<div id="' + div + '" style="display:none;">&nbsp;</div>');
} }
@@ -61,12 +63,13 @@ var commonModule = (function ($, p4) {
left: 0 left: 0
}; };
if (parseInt(zIndex) > 0) if (parseInt(zIndex) > 0) {
css['zIndex'] = parseInt(zIndex); css['zIndex'] = parseInt(zIndex);
}
if (typeof(callback) != 'function') if (typeof(callback) != 'function') {
callback = function () { callback = function () {};
}; }
$('#' + div).css(css).addClass('overlay').fadeTo(500, 0.7).bind('click', function () { $('#' + div).css(css).addClass('overlay').fadeTo(500, 0.7).bind('click', function () {
(callback)(); (callback)();
}); });
@@ -85,8 +88,9 @@ var commonModule = (function ($, p4) {
}); });
} }
var div = "OVERLAY"; var div = "OVERLAY";
if (typeof(n) != "undefined") if (typeof(n) != "undefined") {
div += n; div += n;
}
$('#' + div).hide().remove(); $('#' + div).hide().remove();
} }
@@ -110,40 +114,46 @@ var commonModule = (function ($, p4) {
}).dialog('open').css({'overflow-x': 'auto', 'overflow-y': 'hidden', 'padding': '0'}); }).dialog('open').css({'overflow-x': 'auto', 'overflow-y': 'hidden', 'padding': '0'});
} }
function manageSession(data)
// @deprecated {
function manageSession(data, showMessages) {
if (typeof(showMessages) == "undefined")
showMessages = false;
if (data.status == 'disconnected' || data.status == 'session') { if (data.status == 'disconnected' || data.status == 'session') {
self.location.replace(self.location.href); self.location.replace(self.location.href);
} }
if (showMessages) {
// add notification in bar
// fill the pseudo-dropdown with pre-formatted list of notifs (10 unread)
//
var box = $('#notification_box'); var box = $('#notification_box');
box.empty().append(data.notifications); box.empty().append(data.notifications_html);
if (box.is(':visible')) if (box.is(':visible')) {
fix_notification_height(); fix_notification_height(); // duplicated, better call notifyLayout.setBoxHeight();
}
if ($('.notification.unread', box).length > 0) { // fill the count of uread (red button)
//
var trigger = $('.notification_trigger'); var trigger = $('.notification_trigger');
if(data.notifications.unread_count > 0) {
$('.counter', trigger) $('.counter', trigger)
.empty() .empty()
.append($('.notification.unread', box).length); .append(data.notifications.unread_count).show();
$('.counter', trigger).css('visibility', 'visible'); }
else {
$('.counter', trigger)
.hide()
.empty();
} }
else
$('.notification_trigger .counter').css('visibility', 'hidden').empty();
if (data.changed.length > 0) { // diplay notification about unread baskets
//
if (data.unread_basket_ids.length > 0) {
var current_open = $('.SSTT.ui-state-active'); var current_open = $('.SSTT.ui-state-active');
var current_sstt = current_open.length > 0 ? current_open.attr('id').split('_').pop() : false; var current_sstt = current_open.length > 0 ? current_open.attr('id').split('_').pop() : false;
var main_open = false; var main_open = false;
for (var i = 0; i != data.changed.length; i++) { for (var i = 0; i != data.unread_basket_ids.length; i++) {
var sstt = $('#SSTT_' + data.changed[i]); var sstt = $('#SSTT_' + data.unread_basket_ids[i]);
if (sstt.size() === 0) { if (sstt.size() === 0) {
if (main_open === false) { if (main_open === false) {
$('#baskets .bloc').animate({'top': 30}, function () { $('#baskets .bloc').animate({'top': 30}, function () {
@@ -153,17 +163,20 @@ var commonModule = (function ($, p4) {
} }
} }
else { else {
if (!sstt.hasClass('active')) if (!sstt.hasClass('active')) {
sstt.addClass('unread'); sstt.addClass('unread');
}
else { else {
$('.alert_datas_changed', $('#SSTT_content_' + data.changed[i])).show(); $('.alert_datas_changed', $('#SSTT_content_' + data.unread_basket_ids[i])).show();
} }
} }
} }
} }
if ('' !== $.trim(data.message)) { if ('' !== $.trim(data.message)) {
if ($('#MESSAGE').length === 0) if ($('#MESSAGE').length === 0) {
$('body').append('<div id="#MESSAGE"></div>'); $('body').append('<div id="#MESSAGE"></div>');
}
$('#MESSAGE') $('#MESSAGE')
.empty() .empty()
.append('<div style="margin:30px 10px;"><h4><b>' + data.message + '</b></h4></div><div style="margin:20px 0px 10px;"><label class="checkbox"><input type="checkbox" class="dialog_remove" />' + language.hideMessage + '</label></div>') .append('<div style="margin:30px 10px;"><h4><b>' + data.message + '</b></h4></div><div style="margin:20px 0px 10px;"><label class="checkbox"><input type="checkbox" class="dialog_remove" />' + language.hideMessage + '</label></div>')
@@ -193,9 +206,26 @@ var commonModule = (function ($, p4) {
}) })
.dialog('open'); .dialog('open');
} }
}
return true; return true;
} }
/**
* duplicated from production_client::notifyLayout.js
*/
function fix_notification_height() {
const $notificationBoxContainer = $('#notification_box');
const not = $('.notification', $notificationBoxContainer);
const n = not.length;
const not_t = $('.notification_title', $notificationBoxContainer);
const n_t = not_t.length;
var h = not.outerHeight() * n + not_t.outerHeight() * n_t;
h = h > 350 ? 350 : h;
$notificationBoxContainer.stop().animate({height: h});
}
return { return {
showOverlay: showOverlay, showOverlay: showOverlay,
hideOverlay: hideOverlay, hideOverlay: hideOverlay,

View File

@@ -201,34 +201,35 @@ function reportDatePicker() {
}); });
} }
// poll only from menu bar
function pollNotifications() { // function pollNotifications() {
$.ajax({ // $.ajax({
type: "POST", // type: "POST",
url: "/session/notifications/", // url: "/session/notifications/",
dataType: 'json', // dataType: 'json',
data: { // data: {
module: 10, // module: 10,
usr: usrId // usr: usrId
}, // },
error: function () { // error: function () {
window.setTimeout("pollNotifications();", 10000); // window.setTimeout("pollNotifications();", 10000);
}, // },
timeout: function () { // timeout: function () {
window.setTimeout("pollNotifications();", 10000); // window.setTimeout("pollNotifications();", 10000);
}, // },
success: function (data) { // success: function (data) {
if (data) { // if (data) {
commonModule.manageSession(data); // commonModule.manageSession(data);
} // }
var t = 120000; // var t = 120000;
if (data.apps && parseInt(data.apps) > 1) { // if (data.apps && parseInt(data.apps) > 1) {
t = Math.round((Math.sqrt(parseInt(data.apps) - 1) * 1.3 * 120000)); // t = Math.round((Math.sqrt(parseInt(data.apps) - 1) * 1.3 * 120000));
} // }
window.setTimeout("pollNotifications();", t); // window.setTimeout("pollNotifications();", t);
return; // return;
} // }
}); // });
}; // };
//
window.setTimeout("pollNotifications();", 10000); // window.setTimeout("pollNotifications();", 10000);

View File

@@ -242,7 +242,12 @@
type: "GET", type: "GET",
url: "/admin/databox/{{ databox.get_sbas_id() }}/informations/documents/", url: "/admin/databox/{{ databox.get_sbas_id() }}/informations/documents/",
dataType: 'json', dataType: 'json',
data: {}, headers: {
'update-session': '0' // polling should not maintain the session alive
// also : use lowercase as recomended / normalized
},
data: {
},
success: function (data) { success: function (data) {
try { try {
if (data.viewname === '') { if (data.viewname === '') {

View File

@@ -205,13 +205,7 @@
<li class="notification_trigger"> <li class="notification_trigger">
<a href="#" style="font-weight:bold;text-decoration:none;"> <a href="#" style="font-weight:bold;text-decoration:none;">
<span> <span>
<button class="counter btn btn-danger" <button class="counter btn btn-danger"></button>&nbsp;{{ 'Notifications' | trans }}
style="visibility:{% if app['events-manager'].get_unread_notifications_number > 0 %}visible{% else %}hidden{% endif %};">
{% if app['events-manager'].get_unread_notifications_number > 0 %}
{{ app['events-manager'].get_unread_notifications_number }}
{% endif %}
</button>
{{ 'Notifications' | trans }}
</span> </span>
</a> </a>
</li> </li>
@@ -277,13 +271,7 @@
<li class="notification_trigger"> <li class="notification_trigger">
<a href="#" style="font-weight:bold;text-decoration:none;"> <a href="#" style="font-weight:bold;text-decoration:none;">
<span> <span>
<button class="counter btn btn-danger" <button class="counter btn btn-danger"></button>&nbsp;{{ 'Notifications' | trans }}
style="visibility:{% if app['events-manager'].get_unread_notifications_number > 0 %}visible{% else %}hidden{% endif %};">
{% if app['events-manager'].get_unread_notifications_number > 0 %}
{{ app['events-manager'].get_unread_notifications_number }}
{% endif %}
</button>
{{ 'Notifications' | trans }}
</span> </span>
</a> </a>
</li> </li>
@@ -293,7 +281,6 @@
{% if app.getAuthenticator().isAuthenticated() and module == "prod" %} {% if app.getAuthenticator().isAuthenticated() and module == "prod" %}
<div style="display:none;z-index:30000;" id="notification_box"> <div style="display:none;z-index:30000;" id="notification_box">
{% set notifications = app['events-manager'].get_notifications %}
{% include 'prod/notifications.html.twig' %} {% include 'prod/notifications.html.twig' %}
</div> </div>
{% endif %} {% endif %}
@@ -430,31 +417,36 @@
}); });
}); });
/**manage session and redirect to login page**/ /**manage session and redirect to login page**/
var usrId = '{{ app.getAuthenticator().user.getId }}' ; // var usrId = '{{ app.getAuthenticator().user.getId }}' ;
var module = '{{ module }}';
function pollNotifications() { function pollNotifications() {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "/session/notifications/", url: "/session/notifications/",
dataType: 'json', dataType: 'json',
data: { data: {
module: 10, 'offset': 0,
usr: usrId 'limit': 10,
'what': 2, // 2 : only unread
}, },
error: function () { error: function (data) {
window.setTimeout("pollNotifications();", 10000); if(data.getResponseHeader('x-phraseanet-end-session')) {
self.location.replace(self.location.href); // refresh will redirect to login
}
}, },
timeout: function () { timeout: function () {
window.setTimeout("pollNotifications();", 10000); window.setTimeout("pollNotifications();", 10000);
}, },
success: function (data) { success: function (data) {
if (data) { // there is no notification bar nor a basket notification if not on prod module
if (module === 'prod' && data) {
commonModule.manageSession(data); commonModule.manageSession(data);
} }
window.setTimeout("pollNotifications();", 60000); window.setTimeout("pollNotifications();", 60000);
return;
} }
}); });
}; }
window.setTimeout("pollNotifications();", 10000); window.setTimeout("pollNotifications();", 2000);
</script> </script>

View File

@@ -99,7 +99,7 @@
{% if basket.getValidation is null %} {% if basket.getValidation is null %}
{% set basket_length = basket.getElements().count() %} {% set basket_length = basket.getElements().count() %}
{% set counter = ( counter | default(0) ) + 1 %} {% set counter = ( counter | default(0) ) + 1 %}
{% set counter_length = baskets_collection.length() %} {% set counter_length = baskets_collection | length %}
<tr class="{% if counter >=4 %}other_basket hidden{% endif %}"> <tr class="{% if counter >=4 %}other_basket hidden{% endif %}">
<td colspan="2"> <td colspan="2">

View File

@@ -101,11 +101,11 @@
lang: "{{ app.locale }}", lang: "{{ app.locale }}",
baseUrl: '{{ app['request'].getUriForPath('/') }}', baseUrl: '{{ app['request'].getUriForPath('/') }}',
basePath: '{{ app.request.basePath|e('js') }}', basePath: '{{ app.request.basePath|e('js') }}',
notify: { {#notify: {#}
url: "{{ path('list_notifications') }}", {# url: "{{ path('list_notifications') }}",#}
moduleId: 1, {# moduleId: 1,#}
userId: {{app.getAuthenticatedUser().getId()}} {# userId: {{app.getAuthenticatedUser().getId()}}#}
}, {#},#}
debug: {% if app.debug %}true{% else %}false{% endif %}, debug: {% if app.debug %}true{% else %}false{% endif %},
initialState: "{{ initialAppState }}", initialState: "{{ initialAppState }}",
geonameServerUrl: '{{ app['geonames.server-uri'] }}', geonameServerUrl: '{{ app['geonames.server-uri'] }}',

View File

@@ -6,7 +6,7 @@
</div> </div>
{% else %} {% else %}
<div class="notification_title"> <div class="notification_title">
<a href="#" class="notification__print-action" data-page="0">{{ 'toutes les notifications' | trans }}</a> <a href="#" class="notification__print-action">{{ 'toutes les notifications' | trans }}</a>
</div> </div>
{% endif %} {% endif %}

View File

@@ -150,7 +150,7 @@
alert("{{ 'phraseanet::erreur: Votre session est fermee, veuillez vous re-authentifier' | trans }}"); alert("{{ 'phraseanet::erreur: Votre session est fermee, veuillez vous re-authentifier' | trans }}");
self.location.replace(self.location.href); self.location.replace(self.location.href);
} }
window.setTimeout("pollNotifications();", 10000); window.setTimeout("pollNotifications();", 60000);
return; return;
} }

View File

@@ -21,35 +21,37 @@ define([
eventManager: _.extend({}, Backbone.Events) eventManager: _.extend({}, Backbone.Events)
}; };
window.pullNotifications = function (){ // pull notification is called from menu bar
$.ajax({
type: "POST", // window.pullNotifications = function (){
url: AdminApp.$scope.data("notif-url"), // $.ajax({
dataType: 'json', // type: "POST",
data: { // url: AdminApp.$scope.data("notif-url"),
module : 3, // dataType: 'json',
usr : AdminApp.$scope.data("usr") // data: {
}, // module : 3,
error: function(){ // usr : AdminApp.$scope.data("usr")
window.setTimeout("pullNotifications();", 10000); // },
}, // error: function(){
timeout: function(){ // window.setTimeout("pullNotifications();", 10000);
window.setTimeout("pullNotifications();", 10000); // },
}, // timeout: function(){
success: function(data){ // window.setTimeout("pullNotifications();", 10000);
if (data) { // },
if (data.status == 'disconnected' || data.status == 'session') { // success: function(data){
self.location.replace(self.location.href); // if (data) {
} // if (data.status == 'disconnected' || data.status == 'session') {
} // self.location.replace(self.location.href);
var t = 120000; // }
if (data.apps && parseInt(data.apps) > 1) { // }
t = Math.round((Math.sqrt(parseInt(data.apps)-1) * 1.3 * 120000)); // var t = 120000;
} // if (data.apps && parseInt(data.apps) > 1) {
window.setTimeout("pullNotifications();", t); // t = Math.round((Math.sqrt(parseInt(data.apps)-1) * 1.3 * 120000));
} // }
}); // window.setTimeout("pullNotifications();", t);
}; // }
// });
// };
window.enableForms = function (forms) { window.enableForms = function (forms) {
forms.bind('submit', function(event){ forms.bind('submit', function(event){
var method = $(this).attr('method'); var method = $(this).attr('method');

View File

@@ -15,7 +15,7 @@ define([
var TaskCollection = Backbone.Collection.extend({ var TaskCollection = Backbone.Collection.extend({
model: TaskModel, model: TaskModel,
url: function () { url: function () {
return "/admin/task-manager/tasks"; return "/admin/task-manager/tasks?update_session=0";
} }
}); });