Skip to content

Commit

Permalink
fixes #19192 - adds mark all as read notification action
Browse files Browse the repository at this point in the history
  • Loading branch information
ohadlevy authored and tbrisker committed Apr 19, 2017
1 parent dd29022 commit ea0d94e
Show file tree
Hide file tree
Showing 16 changed files with 255 additions and 47 deletions.
13 changes: 13 additions & 0 deletions app/controllers/notification_recipients_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ def destroy
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
Expand Down
2 changes: 1 addition & 1 deletion app/services/foreman/access_permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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
Expand Down
6 changes: 5 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -517,5 +517,9 @@
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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@
],
"moduleNameMapper": {
"^.+\\.(png|gif|css|scss)$": "identity-obj-proxy"
},
"globals": {
"__testing__": true
}
}
}
42 changes: 41 additions & 1 deletion test/controllers/notification_recipients_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,50 @@ class NotificationRecipientsControllerTest < ActionController::TestCase
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
Expand Down
12 changes: 12 additions & 0 deletions webpack/assets/javascripts/react_app/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ export default {
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);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default (
expandedGroup,
onExpandGroup,
onMarkAsRead,
onMarkGroupAsRead,
onClickedLink
}
) => {
Expand All @@ -17,6 +18,7 @@ export default (
key={key}
onClickedLink={onClickedLink}
onMarkAsRead={onMarkAsRead}
onMarkGroupAsRead={onMarkGroupAsRead}
isExpanded={expandedGroup === key}
onExpand={onExpandGroup}
notifications={notificationGroups[key]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default (
isExpanded,
onExpand,
onMarkAsRead,
onMarkGroupAsRead,
onClickedLink
}
) => {
Expand Down Expand Up @@ -42,6 +43,14 @@ export default (
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>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class notificationContainer extends React.Component {
expandGroup,
expandedGroup,
onMarkAsRead,
onMarkGroupAsRead,
hasUnreadMessages,
isReady,
onClickedLink
Expand All @@ -34,6 +35,7 @@ class notificationContainer extends React.Component {
onExpandGroup={expandGroup}
onClickedLink={onClickedLink}
onMarkAsRead={onMarkAsRead}
onMarkGroupAsRead={onMarkGroupAsRead}
expandedGroup={expandedGroup}
notificationGroups={notifications}
/>}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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,
Expand All @@ -28,7 +28,11 @@ describe('notifications', () => {
};

$.getJSON = jest.genMockFunction().mockImplementation(url => {
return new Promise(resolve => resolve(JSON.parse(serverResponse)));
return {
then: function (callback) {
callback(JSON.parse(serverResponse));
}
};
});
});

Expand All @@ -52,9 +56,7 @@ describe('notifications', () => {
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', () => {
Expand All @@ -64,36 +66,28 @@ describe('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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
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
Expand Down Expand Up @@ -52,6 +53,16 @@ export const onMarkAsRead = (group, id) => dispatch => {
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;

Expand Down
2 changes: 2 additions & 0 deletions webpack/assets/javascripts/react_app/redux/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export const NOTIFICATIONS_SET_EXPANDED_GROUP =
'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';
8 changes: 6 additions & 2 deletions webpack/assets/javascripts/react_app/redux/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import reducer from './reducers';

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;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
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';
Expand All @@ -25,12 +26,19 @@ export default (state = initialState, action) => {
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:
Expand Down
Loading

0 comments on commit ea0d94e

Please sign in to comment.