WIP - Notifications explorer (#2071)

This commit is contained in:
Christophe Maudoux 2020-02-09 17:47:25 +01:00
parent 03b4da1286
commit 2f04ffcc4a
19 changed files with 510 additions and 8 deletions

View File

@ -92,6 +92,38 @@ sub get {
return $result;
}
# Returns accepted notifications corresponding to the user $uid.
# If $ref is set, returns only notification corresponding to this reference.
sub getAccepted {
my ( $self, $uid, $ref ) = @_;
return () unless ($uid);
$self->_execute(
"SELECT * FROM "
. $self->dbiTable
. " WHERE done IS NOT NULL AND uid=?"
. ( $ref ? " AND ref=?" : '' )
. " ORDER BY date",
$uid,
( $ref ? $ref : () )
) or return ();
my $result;
while ( my $h = $self->sth->fetchrow_hashref() ) {
# Get XML message
my $xml = $h->{xml};
# Decode it to get the correct uncoded string
Encode::from_to( $xml, "utf8", "iso-8859-1", Encode::FB_CROAK );
# Store message in result
my $identifier =
&getIdentifier( $self, $h->{uid}, $h->{ref}, $h->{date} );
$result->{$identifier} = $xml;
}
$self->logger->warn( $self->sth->err() ) if ( $self->sth->err() );
return $result;
}
## @method hashref getAll()
# Return all pending notifications.
# @return hashref where keys are internal reference and values are hashref with

View File

@ -58,6 +58,30 @@ sub get {
return $files;
}
# Returns accepted notification corresponding to the user $uid.
# If $ref is set, returns only notification corresponding to this reference.
sub getAccepted {
my ( $self, $uid, $ref ) = @_;
return () unless ($uid);
my $fns = $self->{fileNameSeparator};
my $identifier = &getIdentifier( $self, $uid, $ref );
opendir D, $self->{dirName};
my @notif = grep /^\d{8}${fns}${identifier}\S*\.done$/, readdir(D);
closedir D;
my $files;
foreach my $file (@notif) {
unless ( open F, '<', $self->{dirName} . "/$file" ) {
$self->logger->error(
"Unable to read notification $self->{dirName}/$file");
next;
}
$files->{$file} = join( '', <F> );
}
return $files;
}
## @method hashref getAll()
# Return all pending notifications.
# @return hashref where keys are internal reference and values are hashref with

View File

@ -87,6 +87,38 @@ sub get {
return $result;
}
# Returns accepted notifications corresponding to the user $uid.
# If $ref is set, returns only notification corresponding to this reference.
sub getAccepted {
my ( $self, $uid, $ref ) = @_;
return () unless ($uid);
my $filter =
'(&(objectClass=applicationProcess)(description={done}*)'
. "(description={uid}$uid)"
. ( $ref ? '(description={ref}' . $ref . ')' : '' ) . ')';
my @entries = _search( $self, $filter );
my $result;
foreach my $entry (@entries) {
my @notifValues = $entry->get_value('description');
my $f = {};
foreach (@notifValues) {
my ( $k, $v ) = ( $_ =~ /\{(.*?)\}(.*)/smg );
$v = decodeLdapValue($v);
$f->{$k} = $v;
}
my $xml = $f->{xml};
utf8::encode($xml);
my $identifier =
&getIdentifier( $self, $f->{uid}, $f->{ref}, $f->{date} );
$result->{$identifier} = "$xml";
$self->logger->info("notification $identifier found");
}
return $result;
}
## @method hashref getAll()
# Return all pending notifications.
# @return hashref where keys are internal reference and values are hashref with

View File

@ -1884,6 +1884,10 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
'default' => 'uid reference date title subtitle text check',
'type' => 'text'
},
'notificationsExplorer' => {
'default' => 1,
'type' => 'bool'
},
'notificationStorage' => {
'default' => 'File',
'type' => 'PerlModule'

View File

@ -1135,6 +1135,11 @@ sub attributes {
type => 'bool',
documentation => 'Notification activation',
},
notificationsExplorer => {
default => 1,
type => 'bool',
documentation => 'Notifications explorer activation',
},
notificationServer => {
default => 0,
type => 'bool',

View File

@ -608,6 +608,7 @@ sub tree {
help => 'notifications.html',
nodes => [
'notification',
'notificationsExplorer',
'oldNotifFormat',
'notificationStorage',
'notificationStorageOptions',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -110,6 +110,63 @@ sub checkForNotifications {
return $form;
}
# Search for accepted notification and if any, returns HTML fragment.
sub viewNotification {
my ( $self, $req, $ref, $epoch ) = @_;
# Look for accepted notifications in database
my $uid = $req->userData->{ $self->notifObject->notifField };
my ( $notifs, $forUser ) =
$self->notifObject->getAcceptedNotifs( $uid, $ref );
my $form;
return 0 unless ($notifs);
# Transform notifications
my $i = 0; # Files count
my @res;
foreach my $file ( values %$notifs ) {
my $json = eval { from_json( $file, { allow_nonref => 1 } ) };
$self->userLogger->warn(
"Bad JSON file: a notification for $uid was not done ($@)")
if ($@);
my $j = 0; # Notifications count
$json = [$json] unless ( ref $json eq 'ARRAY' );
LOOP: foreach my $notif ( @{$json} ) {
# Get the reference
my $reference = $notif->{reference};
$self->logger->debug("Get reference: $reference");
# Check it in session
unless (exists $req->{userData}->{"notification_$reference"}
and $req->{userData}->{"notification_$reference"} eq $epoch
and $reference eq $ref )
{
# The notification is not already accepted
$self->logger->debug(
"Notification $reference is not already accepted");
next LOOP;
}
push @res, $notif;
$j++;
}
# Go to next file if no notification found
next unless $j;
$i++;
}
$form .= $self->toForm( $req, @res );
# Stop here if nothing to display
return 0 unless $i;
$self->userLogger->info("$i accepted notification(s) found for $uid");
# Returns HTML fragment
return $form;
}
sub getNotifBack {
my ( $self, $req, $name ) = @_;

View File

@ -158,6 +158,73 @@ sub checkForNotifications {
return $form;
}
# Search for accepted notification and if any, returns HTML fragment.
sub viewNotification {
my ( $self, $req, $ref, $epoch ) = @_;
# Look for pending notifications in database
my $uid = $req->sessionInfo->{ $self->notifObject->notifField };
my ( $notifs, $forUser ) =
$self->notifObject->getAcceptedNotifs( $uid, $ref );
my $form;
return 0 unless ($notifs);
# Transform notifications
my $i = 0; # Files count
foreach my $file ( values %$notifs ) {
my $xml = $self->parser->parse_string($file);
my $j = 0; # Notifications count
LOOP: foreach my $notif (
eval {
$xml->documentElement->getElementsByTagName('notification');
}
)
{
# Get the reference
my $reference = $notif->getAttribute('reference');
$self->logger->debug("Get reference $reference");
# Check it in session
unless (exists $req->{sessionInfo}->{"notification_$reference"}
and $req->{userData}->{"notification_$reference"} eq $epoch
and $reference eq $ref )
{
# The notification is not already accepted
$self->logger->debug(
"Notification $reference was already accepted");
# Remove it from XML
$notif->unbindNode();
next LOOP;
}
$j++;
}
# Go to next file if no notification found
next unless $j;
$i++;
# Transform XML into HTML
my $results = $self->stylesheet->transform( $xml, start => $i );
$form .= $self->stylesheet->output_string($results);
}
if ($@) {
$self->userLogger->warn(
"Bad XML file: a notification for $uid was not done ($@)");
return 0;
}
# Stop here if nothing to display
return 0 unless $i;
$self->userLogger->info("$i accepted notification(s) found for $uid");
# Returns HTML fragment
return $form;
}
sub getNotifBack {
my ( $self, $req, $name ) = @_;

View File

@ -133,6 +133,15 @@ sub params {
: '';
$self->logger->debug("Display DecryptValue link") if $res{decryptValue};
# Display NotifsExplorer link if allowed
my $notifsPlugin =
$self->p->loadedModules->{'Lemonldap::NG::Portal::Plugins::Notifications'};
$res{notifsExplorer} =
$notifsPlugin
? $notifsPlugin->displayLink( $req, $req->userData )
: '';
$self->logger->debug("Display NotifsExplorer link") if $res{notifsExplorer};
return %res;
}

View File

@ -15,12 +15,12 @@ use strict;
use Mouse;
use MIME::Base64;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_ERROR
PE_NOTIFICATION
PE_ERROR
PE_OK
);
our $VERSION = '2.0.6';
our $VERSION = '2.0.8';
extends 'Lemonldap::NG::Portal::Main::Plugin';
@ -33,17 +33,16 @@ use constant endAuth => 'checkNotifDuringAuth';
#sub forAuthUser { 'checkNotifForAuthUser' }
# PROPERTIES
has module => ( is => 'rw' );
# INITIALIZATION
sub init {
my ($self) = @_;
# Declare new route
$self->addUnauthRoute( 'notifback' => 'getNotifBack', [ 'POST', 'GET' ] );
$self->addAuthRoute( 'notifback' => 'getNotifBack', ['POST'] );
$self->addUnauthRoute( notifback => 'getNotifBack', [ 'POST', 'GET' ] );
$self->addAuthRoute( notifback => 'getNotifBack', ['POST'] )
->addAuthRoute( mynotifs => { '*' => 'myNotifs' }, ['GET'] );
if ( $self->conf->{notificationServer} ) {
$self->logger->debug('Notification server enable');
@ -160,4 +159,76 @@ sub notificationServer {
return $self->module->notificationServer( $req, @args );
}
sub myNotifs {
my ( $self, $req, $ref ) = @_;
if ($ref) {
return $self->sendJSONresponse( $req, { error => 'Missing parameter' } )
unless $req->param('epoch');
my $notif = $self->_viewNotif( $req, $ref, $req->param('epoch') );
return $self->sendJSONresponse( $req,
{ notification => $notif, result => ( $notif ? 1 : 0 ) } );
}
my $_notifications = $self->retrieveNotifs($req);
# Display form
my $params = {
PORTAL => $self->conf->{portal},
MAIN_LOGO => $self->conf->{portalMainLogo},
SKIN => $self->p->getSkin($req),
LANGS => $self->conf->{showLanguages},
MSG => 'myNofifications',
DISPLAY => 1,
ACTION => 1,
NOTIFICATIONS => $_notifications
};
return $self->p->sendHtml( $req, 'notifications', params => $params );
}
sub retrieveNotifs {
my ( $self, $req ) = @_;
# Retrieve user's accepted notifications
my $_notifications = [
map {
/^notification_(.+)$/
? { reference => $1, epoch => $req->{userData}->{$_} }
: ()
}
keys %{ $req->{userData} }
];
@$_notifications = sort {
$a->{reference} cmp $b->{reference}
or $a->{epoch} <=> $b->{epoch}
} @$_notifications;
$self->logger->debug(
scalar @$_notifications . " accepted notifications found" );
return $_notifications;
}
sub _viewNotif {
my ( $self, $req, $ref, $epoch ) = @_;
eval {
$req->{data}->{notification} =
$self->module->viewNotification( $req, $ref, $epoch );
};
if ($@) {
$self->logger->error($@);
return '';
}
return $req->{data}->{notification};
}
sub displayLink {
my ( $self, $req ) = @_;
my $_notifications = $self->retrieveNotifs($req);
return ( $self->conf->{notificationsExplorer} && scalar @$_notifications );
}
1;

View File

@ -0,0 +1,50 @@
###
LemonLDAP::NG Notifications script
###
setMsg = (msg, level) ->
$('#msg').html window.translate msg
$('#color').removeClass 'message-positive message-warning alert-success alert-warning'
$('#color').addClass "message-#{level}"
level = 'success' if level == 'positive'
$('#color').addClass "alert-#{level}"
displayError = (j, status, err) ->
console.log 'Error', err
res = JSON.parse j.responseText
if res and res.error
console.log 'Returned error', res
setMsg res, 'warning'
# Delete function (launched by "view" button)
viewNotif = (notif, epoch) ->
console.log 'Ref:', notif, 'epoch:', epoch
if notif and epoch
console.log 'Send AJAX request'
$.ajax
type: "GET"
url: "#{portal}mynotifs/#{notif}"
data:
epoch: epoch
dataType: 'json'
#error: displayError
success: (resp) ->
if resp.result
console.log 'Notification:', resp.notification
$('#displayNotif').html resp.notification
$("#myNotification").removeAttr('hidden')
$('#notifRef').text(notif)
myDate = new Date(epoch * 1000)
$('#notifEpoch').text(myDate.toLocaleString())
else setMsg 'notifNotFound', 'warning'
#error: displayError
else setMsg 'retreiveNotifFailed', 'warning'
# Register "click" events
$(document).ready ->
#$("#myNotification").hide()
$('body').on 'click', '.btn-success', () -> viewNotif ( $(this).attr 'notif' ), ( $(this).attr 'epoch' )
$('#goback').attr 'href', portal
$(".data-epoch").each ->
myDate = new Date($(this).text() * 1000)
$(this).text(myDate.toLocaleString())

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,72 @@
// Generated by CoffeeScript 1.12.7
/*
LemonLDAP::NG Notifications script
*/
(function() {
var displayError, setMsg, viewNotif;
setMsg = function(msg, level) {
$('#msg').html(window.translate(msg));
$('#color').removeClass('message-positive message-warning alert-success alert-warning');
$('#color').addClass("message-" + level);
if (level === 'positive') {
level = 'success';
}
return $('#color').addClass("alert-" + level);
};
displayError = function(j, status, err) {
var res;
console.log('Error', err);
res = JSON.parse(j.responseText);
if (res && res.error) {
console.log('Returned error', res);
return setMsg(res, 'warning');
}
};
viewNotif = function(notif, epoch) {
console.log('Ref:', notif, 'epoch:', epoch);
if (notif && epoch) {
console.log('Send AJAX request');
return $.ajax({
type: "GET",
url: portal + "mynotifs/" + notif,
data: {
epoch: epoch
},
dataType: 'json',
success: function(resp) {
var myDate;
if (resp.result) {
console.log('Notification:', resp.notification);
$('#displayNotif').html(resp.notification);
$("#myNotification").removeAttr('hidden');
$('#notifRef').text(notif);
myDate = new Date(epoch * 1000);
return $('#notifEpoch').text(myDate.toLocaleString());
} else {
return setMsg('notifNotFound', 'warning');
}
}
});
} else {
return setMsg('retreiveNotifFailed', 'warning');
}
};
$(document).ready(function() {
$('body').on('click', '.btn-success', function() {
return viewNotif($(this).attr('notif'), $(this).attr('epoch'));
});
$('#goback').attr('href', portal);
return $(".data-epoch").each(function() {
var myDate;
myDate = new Date($(this).text() * 1000);
return $(this).text(myDate.toLocaleString());
});
});
}).call(this);

View File

@ -0,0 +1 @@
(function(){var a,t;a=function(t,e){return $("#msg").html(window.translate(t)),$("#color").removeClass("message-positive message-warning alert-success alert-warning"),$("#color").addClass("message-"+e),"positive"===e&&(e="success"),$("#color").addClass("alert-"+e)},t=function(o,n){return console.log("Ref:",o,"epoch:",n),o&&n?(console.log("Send AJAX request"),$.ajax({type:"GET",url:portal+"mynotifs/"+o,data:{epoch:n},dataType:"json",success:function(t){var e;return t.result?(console.log("Notification:",t.notification),$("#displayNotif").html(t.notification),$("#myNotification").removeAttr("hidden"),$("#notifRef").text(o),e=new Date(1e3*n),$("#notifEpoch").text(e.toLocaleString())):a("notifNotFound","warning")}})):a("retreiveNotifFailed","warning")},$(document).ready(function(){return $("body").on("click",".btn-success",function(){return t($(this).attr("notif"),$(this).attr("epoch"))}),$("#goback").attr("href",portal),$(".data-epoch").each(function(){var t;return t=new Date(1e3*$(this).text()),$(this).text(t.toLocaleString())})})}).call(this);

View File

@ -0,0 +1 @@
{"version":3,"sources":["lemonldap-ng-portal/site/htdocs/static/common/js/notifications.js"],"names":["setMsg","viewNotif","msg","level","$","html","window","translate","removeClass","addClass","notif","epoch","console","log","ajax","type","url","portal","data","dataType","success","resp","myDate","result","notification","removeAttr","text","Date","toLocaleString","document","ready","on","this","attr","each","call"],"mappings":"CAMA,WACE,IAAkBA,EAAQC,EAE1BD,EAAS,SAASE,EAAKC,GAOrB,OANAC,EAAE,QAAQC,KAAKC,OAAOC,UAAUL,IAChCE,EAAE,UAAUI,YAAY,gEACxBJ,EAAE,UAAUK,SAAS,WAAaN,GACpB,aAAVA,IACFA,EAAQ,WAEHC,EAAE,UAAUK,SAAS,SAAWN,IAazCF,EAAY,SAASS,EAAOC,GAE1B,OADAC,QAAQC,IAAI,OAAQH,EAAO,SAAUC,GACjCD,GAASC,GACXC,QAAQC,IAAI,qBACLT,EAAEU,KAAK,CACZC,KAAM,MACNC,IAAKC,OAAS,YAAcP,EAC5BQ,KAAM,CACJP,MAAOA,GAETQ,SAAU,OACVC,QAAS,SAASC,GAChB,IAAIC,EACJ,OAAID,EAAKE,QACPX,QAAQC,IAAI,gBAAiBQ,EAAKG,cAClCpB,EAAE,iBAAiBC,KAAKgB,EAAKG,cAC7BpB,EAAE,mBAAmBqB,WAAW,UAChCrB,EAAE,aAAasB,KAAKhB,GACpBY,EAAS,IAAIK,KAAa,IAARhB,GACXP,EAAE,eAAesB,KAAKJ,EAAOM,mBAE7B5B,EAAO,gBAAiB,eAK9BA,EAAO,sBAAuB,YAIzCI,EAAEyB,UAAUC,MAAM,WAKhB,OAJA1B,EAAE,QAAQ2B,GAAG,QAAS,eAAgB,WACpC,OAAO9B,EAAUG,EAAE4B,MAAMC,KAAK,SAAU7B,EAAE4B,MAAMC,KAAK,YAEvD7B,EAAE,WAAW6B,KAAK,OAAQhB,QACnBb,EAAE,eAAe8B,KAAK,WAC3B,IAAIZ,EAEJ,OADAA,EAAS,IAAIK,KAAsB,IAAjBvB,EAAE4B,MAAMN,QACnBtB,EAAE4B,MAAMN,KAAKJ,EAAOM,wBAI9BO,KAAKH"}

View File

@ -79,6 +79,12 @@
<span trspan="decryptCipheredValue">decryptCipheredValue</span>
</a></li>
</TMPL_IF>
<TMPL_IF NAME="notifsExplorer">
<li class="dropdown-item"><a href="/mynotifs" class="nav-link">
<img src="<TMPL_VAR NAME="STATIC_PREFIX">common/icons/notifsExplorer.png" width="20" height="20" alt="notifsExplorer" />
<span trspan="notifsExplorer">notifsExplorer</span>
</a></li>
</TMPL_IF>
<li class="dropdown-item"><a href="/refresh" class="nav-link">
<img src="<TMPL_VAR NAME="STATIC_PREFIX">common/icons/arrow_refresh.png" width="16" height="16" alt="refresh" />
<span trspan="refreshrights">Refresh</span>

View File

@ -0,0 +1,70 @@
<TMPL_INCLUDE NAME="header.tpl">
<div class="container">
<div id="color" class="message message-positive alert">
<span id="msg" trspan='<TMPL_VAR NAME="MSG">'></span>
</div>
<TMPL_IF NAME="NOTIFICATIONS">
<div class="card mb-3 border-secondary">
<div class="card-body table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th><span trspan="reference">Reference</span></th>
<th><span trspan="date">Date</span></th>
<th>
<TMPL_IF NAME="ACTION">
<span trspan="action">Action</span>
</TMPL_IF>
</th>
</tr>
</thead>
<tbody>
<TMPL_LOOP NAME="NOTIFICATIONS">
<tr id='delete-<TMPL_VAR NAME="epoch">'>
<td class="align-middle"><TMPL_VAR NAME="reference"></td>
<td class="data-epoch"><TMPL_VAR NAME="epoch"></td>
<td>
<TMPL_IF NAME="DISPLAY">
<span notif='<TMPL_VAR NAME="reference">' epoch='<TMPL_VAR NAME="epoch">' class="btn btn-success" role="button">
<span class="fa fa-eye"></span>
<span trspan="view">View</span>
</span>
</TMPL_IF>
</td>
</tr>
</TMPL_LOOP>
</tbody>
</table>
</div>
</div>
<div class="card mb-3 border-info" id='myNotification' hidden>
<div class="card-header text-white bg-info">
<h3 class="card-title"><span trspan="reference">Reference</span>: <span id="notifRef"></span> / <span trspan="validationDate">Validation date</span>: <span id="notifEpoch"></span></h3>
</div>
<div class="card-body">
<div class="form">
<span id='displayNotif'></span>
</div>
</div>
</div>
</TMPL_IF>
</div>
<div class="buttons">
<a href="<TMPL_VAR NAME="PORTAL_URL">?cancel=1&skin=<TMPL_VAR NAME="SKIN">" class="btn btn-primary" role="button">
<span class="fa fa-home"></span>
<span trspan="goToPortal">Go to portal</span>
</a>
</div>
<!-- //if:jsminified
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/notifications.min.js"></script>
//else -->
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/notifications.js"></script>
<!-- //endif -->
<TMPL_INCLUDE NAME="footer.tpl">