Revision ea0d94e6
Added by Ohad Levy about 7 years ago
app/controllers/notification_recipients_controller.rb | ||
---|---|---|
process_response @notification_recipient.destroy
|
||
end
|
||
|
||
def update_group_as_read
|
||
count = NotificationRecipient.
|
||
joins(:notification_blueprint).
|
||
where(user_id: User.current.id, seen: false,
|
||
notification_blueprints: { group: params[:group]}).
|
||
update_all(seen: true)
|
||
|
||
logger.debug("updated #{count} notification recipents as seen for group #{params[:group]}")
|
||
UINotifications::CacheHandler.new(User.current.id).clear unless count.zero?
|
||
|
||
head status: (count.zero? ? :not_modified : :ok)
|
||
end
|
||
|
||
private
|
||
|
||
def require_login
|
app/services/foreman/access_permissions.rb | ||
---|---|---|
permission_set.security_block :public do |map|
|
||
map.permission :user_logout, { :users => [:logout] }, :public => true
|
||
map.permission :my_account, { :users => [:edit],
|
||
:notification_recipients => [:index, :update, :destroy] }, :public => true
|
||
:notification_recipients => [:index, :update, :destroy, :update_group_as_read] }, :public => true
|
||
map.permission :api_status, { :"api/v1/home" => [:status], :"api/v2/home" => [:status]}, :public => true
|
||
map.permission :about_index, { :about => [:index] }, :public => true
|
||
end
|
config/routes.rb | ||
---|---|---|
end
|
||
end
|
||
|
||
resources :notification_recipients, :only => [:index, :update, :destroy]
|
||
resources :notification_recipients, :only => [:index, :update, :destroy] do
|
||
collection do
|
||
put 'group/:group' => 'notification_recipients#update_group_as_read'
|
||
end
|
||
end
|
||
end
|
package.json | ||
---|---|---|
],
|
||
"moduleNameMapper": {
|
||
"^.+\\.(png|gif|css|scss)$": "identity-obj-proxy"
|
||
},
|
||
"globals": {
|
||
"__testing__": true
|
||
}
|
||
}
|
||
}
|
test/controllers/notification_recipients_controller_test.rb | ||
---|---|---|
assert_equal "#{host} has no owner set", response['notifications'][0]["text"]
|
||
end
|
||
|
||
test 'group mark as read' do
|
||
add_notification
|
||
query = {user_id: User.current.id, seen: false}
|
||
assert_equal 1, NotificationRecipient.where(query).count
|
||
put :update_group_as_read, { :group => 'Testing' }, set_session_user
|
||
assert_response :success
|
||
assert_equal 1, NotificationRecipient.where(query.merge({seen: true})).count
|
||
assert_equal 0, NotificationRecipient.where(query).count
|
||
end
|
||
|
||
test 'group mark as read twice' do
|
||
add_notification
|
||
put :update_group_as_read, { :group => 'Testing' }, set_session_user
|
||
assert_response :success
|
||
put :update_group_as_read, { :group => 'Testing' }, set_session_user
|
||
assert_response :not_modified
|
||
end
|
||
|
||
test 'invalid group mark as read' do
|
||
put :update_group_as_read, { :group => 'unknown;INSERT INTO users (user_id, group)' }, set_session_user
|
||
assert_response :not_modified
|
||
end
|
||
|
||
test 'group mark as read only update the correct group' do
|
||
add_notification('Group1')
|
||
add_notification('Group2')
|
||
query = {user_id: User.current.id, seen: false}
|
||
assert_equal 2, NotificationRecipient.where(query).count
|
||
put :update_group_as_read, { :group => 'Group1' }, set_session_user
|
||
assert_response :success
|
||
assert_equal 1, NotificationRecipient.where(query.merge({seen: true})).count
|
||
assert_equal 0, NotificationRecipient.where(query).
|
||
joins(:notification_blueprint).
|
||
where(notification_blueprints: { group: 'Group1' }).count
|
||
assert_equal 1, NotificationRecipient.where(query).
|
||
joins(:notification_blueprint).
|
||
where(notification_blueprints: { group: 'Group2' }).count
|
||
end
|
||
|
||
private
|
||
|
||
def add_notification
|
||
def add_notification(group = 'Testing')
|
||
type = FactoryGirl.create(:notification_blueprint,
|
||
:group => group,
|
||
:message => 'this test just executed successfully')
|
||
FactoryGirl.create(:notification, :notification_blueprint => type, :audience => 'global')
|
||
end
|
webpack/assets/javascripts/react_app/API.js | ||
---|---|---|
dataType: 'json',
|
||
data: data,
|
||
error: function (jqXHR, textStatus, errorThrown) {
|
||
/* eslint-disable no-console */
|
||
console.log(jqXHR);
|
||
}
|
||
});
|
||
},
|
||
markGroupNotificationAsRead(group) {
|
||
$.ajax({
|
||
url: `/notification_recipients/group/${group}`,
|
||
contentType: 'application/json',
|
||
type: 'PUT',
|
||
error: function (jqXHR, textStatus, errorThrown) {
|
||
/* eslint-disable no-console */
|
||
console.log(jqXHR);
|
||
}
|
||
});
|
webpack/assets/javascripts/react_app/components/notifications/drawer/index.js | ||
---|---|---|
expandedGroup,
|
||
onExpandGroup,
|
||
onMarkAsRead,
|
||
onMarkGroupAsRead,
|
||
onClickedLink
|
||
}
|
||
) => {
|
||
... | ... | |
key={key}
|
||
onClickedLink={onClickedLink}
|
||
onMarkAsRead={onMarkAsRead}
|
||
onMarkGroupAsRead={onMarkGroupAsRead}
|
||
isExpanded={expandedGroup === key}
|
||
onExpand={onExpandGroup}
|
||
notifications={notificationGroups[key]}
|
webpack/assets/javascripts/react_app/components/notifications/drawer/notificationGroup.js | ||
---|---|---|
isExpanded,
|
||
onExpand,
|
||
onMarkAsRead,
|
||
onMarkGroupAsRead,
|
||
onClickedLink
|
||
}
|
||
) => {
|
||
... | ... | |
onMarkAsRead={onMarkAsRead.bind(this, group)}
|
||
/>
|
||
))}
|
||
<div className="drawer-pf-action">
|
||
<a
|
||
className="btn btn-link btn-block"
|
||
onClick={onMarkGroupAsRead.bind(this, group)}
|
||
disabled={unreadCount === 0}>
|
||
{__('Mark All Read')}
|
||
</a>
|
||
</div>
|
||
</div>}
|
||
</div>
|
||
);
|
webpack/assets/javascripts/react_app/components/notifications/index.js | ||
---|---|---|
expandGroup,
|
||
expandedGroup,
|
||
onMarkAsRead,
|
||
onMarkGroupAsRead,
|
||
hasUnreadMessages,
|
||
isReady,
|
||
onClickedLink
|
||
... | ... | |
onExpandGroup={expandGroup}
|
||
onClickedLink={onClickedLink}
|
||
onMarkAsRead={onMarkAsRead}
|
||
onMarkGroupAsRead={onMarkGroupAsRead}
|
||
expandedGroup={expandedGroup}
|
||
notificationGroups={notifications}
|
||
/>}
|
webpack/assets/javascripts/react_app/components/notifications/notifications.test.js | ||
---|---|---|
import React from 'react';
|
||
import { shallow, mount } from 'enzyme';
|
||
import {shallow, mount} from 'enzyme';
|
||
import Notifications from './';
|
||
import thunk from 'redux-thunk';
|
||
import configureMockStore from 'redux-mock-store';
|
||
import Store from '../../redux';
|
||
import {getStore} from '../../redux';
|
||
import {
|
||
emptyState,
|
||
emptyHtml,
|
||
... | ... | |
};
|
||
|
||
$.getJSON = jest.genMockFunction().mockImplementation(url => {
|
||
return new Promise(resolve => resolve(JSON.parse(serverResponse)));
|
||
return {
|
||
then: function (callback) {
|
||
callback(JSON.parse(serverResponse));
|
||
}
|
||
};
|
||
});
|
||
});
|
||
|
||
... | ... | |
const store = mockStore(stateWithNotifications);
|
||
const box = shallow(<Notifications store={store} />);
|
||
|
||
expect(
|
||
box.render().find('.drawer-pf-notification').length
|
||
).toEqual(1);
|
||
expect(box.render().find('.drawer-pf-notification').length).toEqual(1);
|
||
});
|
||
|
||
it('should display full bell on a state with unread notifications', () => {
|
||
... | ... | |
expect(box.render().find('.fa-bell').length).toBe(1);
|
||
});
|
||
|
||
it('full flow', done => {
|
||
const data = { url: '/notification_recipients' };
|
||
const wrapper = mount(<Notifications data={data} store={Store} />);
|
||
|
||
try {
|
||
expect(wrapper.render().find('.fa-bell-o').length).toBe(1);
|
||
setTimeout(() => {
|
||
const rendered = wrapper.render();
|
||
|
||
// full bell is rendered
|
||
expect(rendered.find('.fa-bell').length).toBe(1);
|
||
wrapper.find('.fa-bell').simulate('click');
|
||
expect(rendered.find('.panel-group').length).toEqual(0);
|
||
|
||
setTimeout(() => {
|
||
// a panel group is rendered (inside the accordion)
|
||
expect(wrapper.find('.panel-group').length).toEqual(1);
|
||
wrapper.find('.panel-group .panel-heading').simulate('click');
|
||
setTimeout(() => {
|
||
expect(wrapper.find('.not-seen').length).toEqual(1);
|
||
wrapper.find('.not-seen').simulate('click');
|
||
setTimeout(() => {
|
||
expect(wrapper.find('.not-seen').length).toEqual(0);
|
||
done();
|
||
});
|
||
});
|
||
});
|
||
});
|
||
} catch (e) {
|
||
done();
|
||
}
|
||
it('full flow', () => {
|
||
const data = {url: '/notification_recipients'};
|
||
const wrapper = mount(<Notifications data={data} store={getStore()} />);
|
||
|
||
wrapper.find('.fa-bell').simulate('click');
|
||
expect(wrapper.find('.panel-group').length).toEqual(1);
|
||
wrapper.find('.panel-group .panel-heading').simulate('click');
|
||
expect(wrapper.find('.not-seen').length).toEqual(1);
|
||
wrapper.find('.not-seen').simulate('click');
|
||
expect(wrapper.find('.not-seen').length).toEqual(0);
|
||
});
|
||
|
||
it('mark group as read flow', () => {
|
||
const data = {url: '/notification_recipients'};
|
||
const wrapper = mount(<Notifications data={data} store={getStore()} />);
|
||
const matcher = '.drawer-pf-action a.btn-link';
|
||
|
||
wrapper.find('.fa-bell').simulate('click');
|
||
wrapper.find('.panel-group .panel-heading').simulate('click');
|
||
expect(wrapper.find(matcher).length).toBe(1);
|
||
expect(wrapper.find(`${matcher}[disabled=true]`).length).toBe(0);
|
||
wrapper.find(matcher).simulate('click');
|
||
expect(wrapper.find(`${matcher}[disabled=true]`).length).toBe(1);
|
||
});
|
||
});
|
webpack/assets/javascripts/react_app/redux/actions/notifications/index.js | ||
---|---|---|
NOTIFICATIONS_GET_NOTIFICATIONS,
|
||
NOTIFICATIONS_TOGGLE_DRAWER,
|
||
NOTIFICATIONS_SET_EXPANDED_GROUP,
|
||
NOTIFICATIONS_MARK_AS_READ
|
||
NOTIFICATIONS_MARK_AS_READ,
|
||
NOTIFICATIONS_MARK_GROUP_AS_READ
|
||
} from '../../consts';
|
||
import {
|
||
notificationsDrawer as sessionStorage
|
||
... | ... | |
API.markNotificationAsRead(id);
|
||
};
|
||
|
||
export const onMarkGroupAsRead = (group) => dispatch => {
|
||
dispatch({
|
||
type: NOTIFICATIONS_MARK_GROUP_AS_READ,
|
||
payload: {
|
||
group
|
||
}
|
||
});
|
||
API.markGroupNotificationAsRead(group);
|
||
};
|
||
|
||
export const expandGroup = group => (dispatch, getState) => {
|
||
const currentExpanded = getState().notifications.expandedGroup;
|
||
|
webpack/assets/javascripts/react_app/redux/consts.js | ||
---|---|---|
'NOTIFICATIONS_SET_EXPANDED_GROUP';
|
||
export const NOTIFICATIONS_MARK_AS_READ =
|
||
'NOTIFICATIONS_MARK_AS_READ';
|
||
export const NOTIFICATIONS_MARK_GROUP_AS_READ =
|
||
'NOTIFICATIONS_MARK_GROUP_AS_READ';
|
webpack/assets/javascripts/react_app/redux/index.js | ||
---|---|---|
|
||
let middleware = [thunk];
|
||
|
||
if (process.env.NODE_ENV !== 'production') {
|
||
if (process.env.NODE_ENV !== 'production' && !global.__testing__) {
|
||
middleware = [...middleware, createLogger()];
|
||
}
|
||
|
||
export default createStore(
|
||
const _getStore = () => createStore(
|
||
reducer,
|
||
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
|
||
applyMiddleware(...middleware)
|
||
);
|
||
|
||
export default _getStore();
|
||
|
||
export const getStore = _getStore;
|
webpack/assets/javascripts/react_app/redux/reducers/notifications/index.js | ||
---|---|---|
NOTIFICATIONS_GET_NOTIFICATIONS,
|
||
NOTIFICATIONS_TOGGLE_DRAWER,
|
||
NOTIFICATIONS_SET_EXPANDED_GROUP,
|
||
NOTIFICATIONS_MARK_AS_READ
|
||
NOTIFICATIONS_MARK_AS_READ,
|
||
NOTIFICATIONS_MARK_GROUP_AS_READ
|
||
} from '../../consts';
|
||
import Immutable from 'seamless-immutable';
|
||
import { notificationsDrawer } from '../../../common/sessionStorage';
|
||
... | ... | |
case NOTIFICATIONS_SET_EXPANDED_GROUP:
|
||
return state.set('expandedGroup', payload.group);
|
||
case NOTIFICATIONS_MARK_AS_READ:
|
||
return state.set(
|
||
'notifications',
|
||
state.notifications.map(
|
||
n => n.id === payload.id ?
|
||
Object.assign({}, n, {seen: true}) : n
|
||
)
|
||
);
|
||
case NOTIFICATIONS_MARK_GROUP_AS_READ:
|
||
return state.set(
|
||
'notifications',
|
||
state.notifications.map(
|
||
n => n.id === payload.id ?
|
||
Object.assign({}, n, {seen: true}) :
|
||
n
|
||
n => n.group === payload.group ?
|
||
Object.assign({}, n, {seen: true}) : n
|
||
)
|
||
);
|
||
default:
|
webpack/assets/javascripts/react_app/redux/reducers/notifications/notifications.fixtures.js | ||
---|---|---|
/* eslint-disable camelcase */
|
||
import Immutable from 'seamless-immutable';
|
||
export const initialState = Immutable(
|
||
{
|
||
expandedGroup: null,
|
||
isDrawerOpen: null
|
||
});
|
||
|
||
export const stateWithNotifications = Immutable(
|
||
{
|
||
isDrawerOpen: true,
|
||
expandedGroup: 'Hosts',
|
||
notifications: [
|
||
{
|
||
id: 52435,
|
||
seen: false,
|
||
level: 'info',
|
||
text: 'some.example.com has been deleted successfully',
|
||
created_at: '2017-04-17T17:29:12.664Z',
|
||
group: 'Hosts',
|
||
actions: {}
|
||
},
|
||
{
|
||
id: 51435,
|
||
seen: false,
|
||
level: 'info',
|
||
text: 'notified successfully',
|
||
created_at: '2017-04-17T17:29:12.664Z',
|
||
group: 'Hosts',
|
||
actions: {}
|
||
},
|
||
{
|
||
id: 52433,
|
||
seen: false,
|
||
level: 'info',
|
||
text: 'notfied successfully',
|
||
created_at: '2017-04-17T17:29:12.664Z',
|
||
group: 'Testing',
|
||
actions: {}
|
||
}
|
||
]
|
||
}
|
||
);
|
||
export const request = {
|
||
group: 'Hosts'
|
||
};
|
||
export const response = Immutable(
|
||
{
|
||
isDrawerOpen: true,
|
||
expandedGroup: 'Hosts',
|
||
notifications: [
|
||
{
|
||
id: 52435,
|
||
seen: true,
|
||
level: 'info',
|
||
text: 'some.example.com has been deleted successfully',
|
||
created_at: '2017-04-17T17:29:12.664Z',
|
||
group: 'Hosts',
|
||
actions: {}
|
||
},
|
||
{
|
||
id: 51435,
|
||
seen: true,
|
||
level: 'info',
|
||
text: 'notified successfully',
|
||
created_at: '2017-04-17T17:29:12.664Z',
|
||
group: 'Hosts',
|
||
actions: {}
|
||
},
|
||
{
|
||
id: 52433,
|
||
seen: false,
|
||
level: 'info',
|
||
text: 'notfied successfully',
|
||
created_at: '2017-04-17T17:29:12.664Z',
|
||
group: 'Testing',
|
||
actions: {}
|
||
}
|
||
]
|
||
}
|
||
);
|
webpack/assets/javascripts/react_app/redux/reducers/notifications/notifications.test.js | ||
---|---|---|
import reducer from './index';
|
||
import * as types from '../../consts';
|
||
import {
|
||
initialState,
|
||
stateWithNotifications,
|
||
request,
|
||
response
|
||
} from './notifications.fixtures';
|
||
|
||
describe('notification reducer', () => {
|
||
it('should return the initial state', () => {
|
||
expect(reducer(undefined, {})).toEqual(initialState);
|
||
});
|
||
|
||
it('should handle NOTIFICATIONS_MARK_GROUP_AS_READ', () => {
|
||
expect(
|
||
reducer(stateWithNotifications, {
|
||
type: types.NOTIFICATIONS_MARK_GROUP_AS_READ,
|
||
payload: request
|
||
})
|
||
).toEqual(response);
|
||
});
|
||
});
|
Also available in: Unified diff
fixes #19192 - adds mark all as read notification action
This implements missing feature from patternfly ( see http://www.patternfly.org/pattern-library/communication/notification-drawer/#/code/angular)