Skip to content
This repository has been archived by the owner on May 4, 2022. It is now read-only.

Commit

Permalink
v3 backport: MUMUP-2875 Personally filtered notifications (#392)
Browse files Browse the repository at this point in the history
  • Loading branch information
apetro committed Apr 12, 2017
1 parent 248e189 commit da56161
Show file tree
Hide file tree
Showing 4 changed files with 538 additions and 119 deletions.
9 changes: 7 additions & 2 deletions docs/notifications.md
Expand Up @@ -57,7 +57,10 @@ point uw-frame to your desired feed.
"actionURL" : "https://www.mynetid.wisc.edu/modify",
"actionAlt" : "Activate Services",
"dismissable" : true,
"priority" : false
"priority" : false,
"dataURL" : "/restProxyURL/unactivatedServices",
"dataObject" : "services",
"dataArrayFilter" : {"priority":"essential", "type":"netid"}
}
]
}
Expand All @@ -80,7 +83,9 @@ This should almost always be true.
"Users - Account Activation Required," which calls on those users to activate their accounts. Upon following through, a user would be removed from that group and the notification would be no longer visible. In this example, the call
to action is reasonably important, and the notification should stick around until the user takes the desired action.
- **priority**: Set to true if the notification is of critical importance. The visibility of the notification will be amplified throughout the UI. **This feature should be used sparingly.**

- **dataURL** (*optional*) : Will retrieve the data from the dataURL. If data exists, will show notification to user, if data does not exist, will not show notification. Only supports JSON. You would use this feature if you want to only show the notification if the user has data. For example, only show user if they have a certain document.
- **dataObject** (*optional*) : Will only be looked at if `dataURL` is present, otherwise ignored. Used as an optional further refinement from dataURL, if you want the notification to show only if the specific object is in the data.
- **dataArrayFilter** (*optional*) : Will only be looked at if `dataURL` is present, otherwise ignored. Used as an optional further refinement from dataURL. If your object return is an array, you can filter on the array. Does support multiple filtering criteria as shown in the example. If used in conjunction with `dataObject`, will filter to `dataObject` first. [AngularJS array filtering documentation] (https://docs.angularjs.org/api/ng/filter/filter)

### Action buttons

Expand Down
2 changes: 1 addition & 1 deletion uw-frame-components/portal/notifications/controllers.js
Expand Up @@ -88,7 +88,7 @@ define(['angular'], function(angular) {
var getNotifications = function() {
dismissedNotificationIds = [];
if(NOTIFICATION.groupFiltering && !$localStorage.disableGroupFilteringForNotifications) {
notificationsService.getNotificationsByGroups().then(getNotificationsSuccess, getNotificationsError);
notificationsService.getFilteredNotifications().then(getNotificationsSuccess, getNotificationsError);
} else {
notificationsService.getAllNotifications().then(getNotificationsSuccess, getNotificationsError);
}
Expand Down
280 changes: 179 additions & 101 deletions uw-frame-components/portal/notifications/services.js
Expand Up @@ -4,68 +4,199 @@ define(['angular', 'jquery'], function(angular, $) {

var app = angular.module('portal.notifications.services', []);

app.factory('notificationsService', ['$q','$http', 'miscService', 'PortalGroupService', 'keyValueService','SERVICE_LOC', 'KV_KEYS', function($q, $http, miscService, PortalGroupService, keyValueService, SERVICE_LOC, KV_KEYS) {
app.factory('notificationsService', ['$q','$http', '$log', '$filter', 'miscService', 'PortalGroupService', 'keyValueService','SERVICE_LOC', 'KV_KEYS', function($q, $http, $log, $filter, miscService, PortalGroupService, keyValueService, SERVICE_LOC, KV_KEYS) {
// LOCAL VARIABLES
var filteredNotificationPromise;
var dismissedPromise;
var dismissedIds = [];

/////////////////////
// SERVICE METHODS //
/////////////////////

/**
* @typedef {Object} Notification
* @property {number} id
* @property {string} title
* @property {string} actionURL
* @property {boolean} dismissable
* @property {boolean} false
* @property {string} actionALt
* @property {string=} dataURL
* @property {string=} dataObject
* @property {Object=} dataArrayFilter
*/

/**
* @typedef {Object} NotificationReturnObject
* @property {Notification[]} dismissed
* @property {Notification[]} notDismissed
*/

/**
* Get all notifications at the given URL (as set in app-config.js or override.js)
* @returns {*} A promise which returns an object containing notifications arrays
* @returns {Promise<NotificationReturnObject>} A promise which returns an object containing notifications arrays
*/
var getAllNotifications = function() {
return $q.all([$http.get(SERVICE_LOC.notificationsURL, {cache : true}), getDismissedNotificationIds()])
.then(function(result) {
return sortNotifications(result[0].data.notifications, result[1]);
},
function(reason) {
return $http.get(SERVICE_LOC.notificationsURL, {cache : true}).then(function(result) {
return separateNotificationsByDismissal(result.data.notifications);
}).then(function(seperatedNotifications){
return seperatedNotifications;
}).catch (function(reason) {
miscService.redirectUser(reason.status, 'notifications json feed call');
}
);
});
};

/**
* Asynchronously fetch notifications and portal groups, then filter notifications by group. Called if group
* filtering is enabled.
* @returns {*} A promise which returns an object containing notifications arrays
* Gets notifications and filters them by group and via data if dataURL is
* present.
* @returns {Promise<NotificationReturnObject>} A promise which returns an object containing notifications arrays
*/
var getNotificationsByGroups = function() {
// If $q already happened, just return the promise
if(filteredNotificationPromise) {
return filteredNotificationPromise;
}
var getFilteredNotifications = function() {
var notifications = {
'dismissed': [],
'notDismissed': []
};
return $http.get(SERVICE_LOC.notificationsURL, {cache : true}).then(function(allNotifications){
return filterNotificationsByGroup(allNotifications.data.notifications);
}).then(function(filteredNotificationsByGroup){
return separateNotificationsByDismissal(filteredNotificationsByGroup);
}).then(function (notificationsBydismissal){
notifications.dismissed = notificationsBydismissal.dismissed;
return filterNotificationsByData(notificationsBydismissal.notDismissed);
}).then(function(notificationsByData){
notifications.notDismissed = notificationsByData;
return notifications;
}).catch (function(reason){
$log.warn("Error in retrieving notifications");
return [];
});
};

/**
* Filter the array of notifications based on the groups that were passed in
* @param {Notification[]} arrayOfNotifications
* @return {Promise<Notification[]>} : an array filtered by groups. Or an empty array if they have none
**/
function filterNotificationsByGroup(arrayOfNotifications) {
return PortalGroupService.getGroups().then(
function(groups){
var notificationsByGroup = [];
angular.forEach(arrayOfNotifications, function (notification, index) {
var added = false;
// For each group for the current notification
angular.forEach(notification.groups, function(group, index) {
if (!added) {
// Intersect, then get length
var inGroup = $.grep(groups, function(e) {return e.name === group}).length;
if (inGroup > 0) {
// If user is in this group, he should see this notification
notificationsByGroup.push(notification);
added = true;
}
}
});
});
return notificationsByGroup;
}).catch (function(reason){
miscService.redirectUser(reason.status, 'Unable to retrieve groups');
}
);
};

/**
* Filter the array of notifications based on if data was requested before showing
* @param {Notification[]} : an array of notifications
* @return Promise<Notification[]>} : an array of notifications that includes only non-data notifications
* and notifications that requested data and had data
**/
var filterNotificationsByData = function(notifications){
var promises = [];
var filteredNotifications = [];

angular.forEach(notifications, function(notification, index){
if (notification.dataURL) {
promises.push($http.get(notification.dataURL).then(
function(result){
var objectToFind = result.data;
//If dataObject specified, try to use it
if (result && notification.dataObject){
objectToFind = objectToFind[notification.dataObject];
}
//If dataArrayFilter specified, then filter
if (objectToFind && notification.dataArrayFilter) {
//If you try to do an array filter on a non-array, return blank
if (!angular.isArray(objectToFind)) {
return;
}
var arrayFilter = angular.fromJson(notification.dataArrayFilter);
objectToFind = $filter('filter')(objectToFind, arrayFilter);
}

//If the data object is there, we have a match
if (objectToFind && objectToFind.length > 0) {
return notification;
}else{
return;
}
}).catch (function(reason){
$log.warn("Error retrieving data for notification");
}
));
}else{
filteredNotifications.push(notification);
}
});

// If $q completed successfully, filter notifications by group
var successFn = function(result) {
//post processing
var groups = result[1],
allNotifications = result[0],
notificationsByGroup = {};
if(groups && allNotifications) {
notificationsByGroup.dismissed = filterNotificationsByGroup(allNotifications.dismissed, groups);
notificationsByGroup.notDismissed = filterNotificationsByGroup(allNotifications.notDismissed, groups);
return $q.all(promises).then(
function(result){
angular.forEach(result, function(notification, index){
if (notification) {
filteredNotifications.push(notification);
}
});
return filteredNotifications;
}
// Return filtered notifications, sorted into dismissed and notDismissed arrays
return notificationsByGroup;
};
// If $q failed, redirect user back to login and give a reason
var errorFn = function(reason) {
miscService.redirectUser(reason.status, 'q for filtered notifications');
);
}


/**
* Separates notification array into new object with two properties
* @param {Notification[]} : an array of notifications
* @return {Promise<Notification[]>}: an object with two properties. dismissed and notDismissed
**/
var separateNotificationsByDismissal = function(notifications){
var separatedNotifications = {
'dismissed': [],
'notDismissed': []
};

// Set up asynchronous calls to get notifications and portal groups
filteredNotificationPromise = $q.all([getAllNotifications(), PortalGroupService.getGroups()]).then(successFn, errorFn);

return filteredNotificationPromise;
return getDismissedNotificationIDs().then(
function(dismissedIDs){
// Check notification IDs against dismissed IDs from k/v store and sort notifications into appropriate array
if (angular.isArray(dismissedIDs)) {
angular.forEach(notifications, function(notification, index) {
if (dismissedIDs.indexOf(notification.id) > -1) {
separatedNotifications.dismissed.push(notification);
} else {
separatedNotifications.notDismissed.push(notification);
}
});
} else {
separatedNotifications.notDismissed = data;
}
// Return sorted notifications
return separatedNotifications;
}).catch (function(reason){
$log.warn("Error in retrieving previously dismissed notifications");
return notifications;
}
);
};


/**
* Save array of dismissed notification IDs in k/v store, reset dismissedPromise
* @param dismissedIds Array of ID strings
* @param {Notification[]} - dismissedIds Array of ID strings
*/
var setDismissedNotifications = function(dismissedIds) {
keyValueService.setValue(KV_KEYS.DISMISSED_NOTIFICATION_IDS, dismissedIds);
Expand All @@ -74,17 +205,17 @@ define(['angular', 'jquery'], function(angular, $) {

/**
* Get array of dismissed notification IDs from k/v store
* @returns {*} A promise that returns an array of IDs
* @returns {Promise<number[]>} A promise that returns an array of IDs
*/
var getDismissedNotificationIds = function() {
if(!keyValueService.isKVStoreActivated()) {
var getDismissedNotificationIDs = function() {
if (!keyValueService.isKVStoreActivated()) {
return $q.resolve([]);
}
dismissedPromise = dismissedPromise || keyValueService.getValue(KV_KEYS.DISMISSED_NOTIFICATION_IDS)
.then(function(data) {
if(data && typeof data.value === 'string') {
if (data && typeof data.value === 'string') {
// If data exists and is a string, check for emptiness
if(data.value) {
if (data.value) {
// If string contains things, return parsed JSON
return JSON.parse(data.value);
} else {
Expand All @@ -102,64 +233,11 @@ define(['angular', 'jquery'], function(angular, $) {
return dismissedPromise;
};

/**
* Sort all notifications into two arrays to be consumed by notifications controller;
* @param data Array of notifications
* @returns {{dismissed: Array, notDismissed: Array}} Object with arrays sorted by dismissal
*/
var sortNotifications = function(data, dismissedIds) {
var notifications = {
'dismissed': [],
'notDismissed': []
};
// Check notification IDs against dismissed IDs from k/v store and sort notifications into appropriate array
if(angular.isArray(dismissedIds)) {
angular.forEach(data, function(notification, index) {
if (dismissedIds.indexOf(notification.id) > -1) {
notifications.dismissed.push(notification);
} else {
notifications.notDismissed.push(notification);
}
});
} else {
notifications.notDismissed = data;
}
// Return sorted notifications
return notifications;
};

/**
* Filter the array of notifications based on the groups that were passed in
* @param arrayOfNotifications : an array of notifications
* @param groups : The list of groups one person is in
* @return : an array filtered by groups. Or an empty array if they have none
**/
function filterNotificationsByGroup(arrayOfNotifications, groups) {
var notificationsByGroup = [];
$.each(arrayOfNotifications, function (index, notification) {
var added = false;
// For each group for the current notification
$.each(notification.groups, function(index, group) {
if(!added) {
// Intersect, then get length
var inGroup = $.grep(groups, function(e) {return e.name === group}).length;
if(inGroup > 0) {
// If user is in this group, he should see this notification
notificationsByGroup.push(notification);
added = true;
}
}
});
});

return notificationsByGroup;
}

return {
getAllNotifications: getAllNotifications,
getNotificationsByGroups : getNotificationsByGroups,
getDismissedNotificationIds : getDismissedNotificationIds,
setDismissedNotifications : setDismissedNotifications
getDismissedNotificationIDs : getDismissedNotificationIDs,
setDismissedNotifications : setDismissedNotifications,
getFilteredNotifications: getFilteredNotifications
};

}]);
Expand Down

0 comments on commit da56161

Please sign in to comment.