dhis2-devs team mailing list archive
-
dhis2-devs team
-
Mailing list archive
-
Message #32967
[Branch ~dhis2-devs-core/dhis2/trunk] Rev 16771: Added batch delete, read, unread for MessageConversations in webapi. Implemented batch operations...
Merge authors:
Halvdan Hoem Grelland (halvdanhg)
------------------------------------------------------------
revno: 16771 [merge]
committer: Halvdan Hoem Grelland <halvdanhg@xxxxxxxxx>
branch nick: dhis2
timestamp: Mon 2014-09-22 17:51:59 +0200
message:
Added batch delete, read, unread for MessageConversations in webapi. Implemented batch operations using these in dashboard messaging app. Added authorisation checks to read and delete MessageConversations.
added:
dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/jQuery/jquery.dhisCheckboxMenu.js
modified:
dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversation.java
dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversationStore.java
dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageService.java
dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/message/DefaultMessageService.java
dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/message/hibernate/HibernateMessageConversationStore.java
dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/message/MessageServiceTest.java
dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MessageConversationController.java
dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/css/widgets.css
dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/java/org/hisp/dhis/dashboard/message/action/ReadMessageAction.java
dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/resources/org/hisp/dhis/dashboard/i18n_module.properties
dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/resources/struts.xml
dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/webapp/dhis-web-dashboard-integration/javascript/message.js
dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/webapp/dhis-web-dashboard-integration/message.vm
--
lp:dhis2
https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk
Your team DHIS 2 developers is subscribed to branch lp:dhis2.
To unsubscribe from this branch go to https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk/+edit-subscription
=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversation.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversation.java 2014-08-15 07:40:20 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversation.java 2014-08-19 11:16:02 +0000
@@ -213,7 +213,7 @@
this.setLastMessage( new Date() );
}
- public void remove( User user )
+ public boolean remove( User user )
{
Iterator<UserMessage> iterator = userMessages.iterator();
@@ -225,9 +225,10 @@
{
iterator.remove();
- return;
+ return true;
}
}
+ return false;
}
public Set<User> getUsers()
=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversationStore.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversationStore.java 2014-03-18 08:10:10 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversationStore.java 2014-08-20 14:37:14 +0000
@@ -31,6 +31,7 @@
import org.hisp.dhis.common.GenericIdentifiableObjectStore;
import org.hisp.dhis.user.User;
+import java.util.Collection;
import java.util.List;
/**
@@ -49,6 +50,14 @@
* @return a list of MessageConversations.
*/
List<MessageConversation> getMessageConversations( User user, boolean followUpOnly, boolean unreadOnly, Integer first, Integer max );
+
+ /**
+ * Returns the MessageConversations given by the supplied UIDs.
+ *
+ * @param messageConversationUids the UIDs of the MessageConversations to get.
+ * @return a collection of MessageConversations.
+ */
+ Collection<MessageConversation> getMessageConversations( String[] messageConversationUids );
int getMessageConversationCount( User user, boolean followUpOnly, boolean unreadOnly );
=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageService.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageService.java 2014-03-18 08:10:10 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageService.java 2014-08-20 14:37:14 +0000
@@ -28,6 +28,7 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
+import java.util.Collection;
import java.util.List;
import java.util.Set;
@@ -93,6 +94,8 @@
List<MessageConversation> getMessageConversations( boolean followUpOnly, boolean unreadOnly, int first, int max );
+ Collection<MessageConversation> getMessageConversations( String[] messageConversationUids );
+
int getMessageConversationCount();
int getMessageConversationCount( boolean followUpOnly, boolean unreadOnly );
=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/message/DefaultMessageService.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/message/DefaultMessageService.java 2014-08-15 07:40:20 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/message/DefaultMessageService.java 2014-08-20 14:37:14 +0000
@@ -28,6 +28,7 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
+import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -272,6 +273,11 @@
unreadOnly, first, max );
}
+ public Collection<MessageConversation> getMessageConversations( String[] messageConversationUids )
+ {
+ return messageConversationStore.getMessageConversations( messageConversationUids );
+ }
+
public int getMessageConversationCount()
{
return messageConversationStore.getMessageConversationCount( currentUserService.getCurrentUser(), false, false );
=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/message/hibernate/HibernateMessageConversationStore.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/message/hibernate/HibernateMessageConversationStore.java 2014-08-13 10:43:22 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/message/hibernate/HibernateMessageConversationStore.java 2014-08-20 14:37:14 +0000
@@ -40,6 +40,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.Collection;
import java.util.List;
/**
@@ -124,6 +125,17 @@
return conversations;
}
+ @Override
+ public Collection<MessageConversation> getMessageConversations( String[] messageConversationUids )
+ {
+ String hql = ( "FROM MessageConversation where uid in :messageConversationUids" );
+
+ Query query = getQuery( hql );
+ query.setParameterList( "messageConversationUids", messageConversationUids );
+
+ return query.list();
+ }
+
public int getMessageConversationCount( User user, boolean followUpOnly, boolean unreadOnly )
{
String sql = "select count(*) from messageconversation mc "
=== modified file 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/message/MessageServiceTest.java'
--- dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/message/MessageServiceTest.java 2014-08-15 07:40:20 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/message/MessageServiceTest.java 2014-09-22 14:26:53 +0000
@@ -29,9 +29,11 @@
*/
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -199,4 +201,31 @@
assertEquals( "Subject", message.getSubject() );
assertEquals( 2, message.getMessages().size() );
}
+
+ @Test
+ public void testGetMessageConversations()
+ {
+ MessageConversation conversationA = new MessageConversation( "SubjectA", sender );
+ MessageConversation conversationB = new MessageConversation( "SubjectB", sender );
+ MessageConversation conversationC = new MessageConversation( "SubjectC", userA );
+
+ messageService.saveMessageConversation( conversationA );
+ messageService.saveMessageConversation( conversationB );
+ messageService.saveMessageConversation( conversationC );
+
+ String uidA = conversationA.getUid();
+ String uidB = conversationB.getUid();
+
+ messageService.saveMessageConversation( conversationA );
+ messageService.saveMessageConversation( conversationB );
+ messageService.saveMessageConversation( conversationC );
+
+ String[] uids = { uidA, uidB };
+
+ Collection<MessageConversation> conversations = messageService.getMessageConversations( uids );
+
+ assertTrue( conversations.contains( conversationA ) );
+ assertTrue( conversations.contains( conversationB ) );
+ assertFalse( conversations.contains( conversationC ) );
+ }
}
=== modified file 'dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MessageConversationController.java'
--- dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MessageConversationController.java 2014-08-15 07:40:20 +0000
+++ dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MessageConversationController.java 2014-09-22 14:26:53 +0000
@@ -29,11 +29,17 @@
*/
import com.google.common.collect.Lists;
+import org.hisp.dhis.acl.AclService;
import org.hisp.dhis.common.Pager;
import org.hisp.dhis.dxf2.message.Message;
import org.hisp.dhis.dxf2.utils.JacksonUtils;
+import org.hisp.dhis.hibernate.exception.DeleteAccessDeniedException;
+import org.hisp.dhis.hibernate.exception.UpdateAccessDeniedException;
import org.hisp.dhis.message.MessageConversation;
import org.hisp.dhis.message.MessageService;
+import org.hisp.dhis.node.types.CollectionNode;
+import org.hisp.dhis.node.types.RootNode;
+import org.hisp.dhis.node.types.SimpleNode;
import org.hisp.dhis.organisationunit.OrganisationUnit;
import org.hisp.dhis.organisationunit.OrganisationUnitService;
import org.hisp.dhis.schema.descriptors.MessageConversationSchemaDescriptor;
@@ -46,17 +52,22 @@
import org.hisp.dhis.webapi.webdomain.WebMetaData;
import org.hisp.dhis.webapi.webdomain.WebOptions;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -96,6 +107,28 @@
}
@Override
+ public RootNode getObject( @PathVariable String uid, Map<String, String> parameters, HttpServletRequest request, HttpServletResponse response )
+ throws Exception
+ {
+ MessageConversation messageConversation = messageService.getMessageConversation( uid );
+
+ if( messageConversation == null )
+ {
+ response.setStatus( HttpServletResponse.SC_NOT_FOUND );
+ RootNode responseNode = new RootNode( "reply" );
+ responseNode.addChild( new SimpleNode( "message", "No MessageConversation found with UID: " + uid ) );
+ return responseNode;
+ }
+
+ if( !canReadMessageConversation( currentUserService.getCurrentUser(), messageConversation ) )
+ {
+ throw new AccessDeniedException( "Not authorized to access this conversation." );
+ }
+
+ return super.getObject( uid, parameters, request, response );
+ }
+
+ @Override
protected List<MessageConversation> getEntityList( WebMetaData metaData, WebOptions options )
{
List<MessageConversation> entityList;
@@ -235,4 +268,260 @@
ContextUtils.createdResponse( response, "Feedback created", null );
}
+
+
+ //--------------------------------------------------------------------------
+ // Mark conversations read
+ //--------------------------------------------------------------------------
+
+ @RequestMapping( value = "/read", method = RequestMethod.PUT, produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE } )
+ public @ResponseBody RootNode markMessageConversationsRead(
+ @RequestParam( value = "user", required = false ) String userUid, @RequestBody String[] uids, HttpServletResponse response )
+ {
+ RootNode responseNode = new RootNode( "response" );
+
+ User currentUser = currentUserService.getCurrentUser();
+ User user = userUid != null ? userService.getUser( userUid ) : currentUserService.getCurrentUser();
+
+ if( user == null )
+ {
+ response.setStatus( HttpServletResponse.SC_NOT_FOUND );
+ responseNode.addChild( new SimpleNode( "message", "No user with uid: " + userUid ) );
+ return responseNode;
+ }
+
+ if( !canModifyUserConversation( currentUser, user ) )
+ {
+ throw new UpdateAccessDeniedException( "Not authorized to modify this object." );
+ }
+
+ Collection<MessageConversation> messageConversations = messageService.getMessageConversations( uids );
+
+ if ( messageConversations.isEmpty() )
+ {
+ response.setStatus( HttpServletResponse.SC_NOT_FOUND );
+ responseNode.addChild( new SimpleNode( "message", "No MessageConversations found for the given UIDs." ) );
+ return responseNode;
+ }
+
+ CollectionNode marked = responseNode.addChild( new CollectionNode( "markedRead" ) );
+ marked.setWrapping( false );
+
+ for( MessageConversation conversation : messageConversations )
+ {
+ if( conversation.markRead( user ) )
+ {
+ messageService.updateMessageConversation( conversation );
+ marked.addChild( new SimpleNode( "uid", conversation.getUid() ) );
+ }
+ }
+
+ response.setStatus( HttpServletResponse.SC_OK );
+
+ return responseNode;
+ }
+
+ //--------------------------------------------------------------------------
+ // Mark conversations unread
+ //--------------------------------------------------------------------------
+
+ @RequestMapping( value = "/unread", method = RequestMethod.PUT, produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE } )
+ public @ResponseBody RootNode markMessageConversationsUnread(
+ @RequestParam( value = "user", required = false ) String userUid, @RequestBody String[] uids, HttpServletResponse response )
+ {
+ RootNode responseNode = new RootNode( "response" );
+
+ User currentUser = currentUserService.getCurrentUser();
+ User user = userUid != null ? userService.getUser( userUid ) : currentUser;
+
+ if( user == null )
+ {
+ response.setStatus( HttpServletResponse.SC_NOT_FOUND );
+ responseNode.addChild( new SimpleNode( "message", "No user with uid: " + userUid ) );
+ return responseNode;
+ }
+
+ if( !canModifyUserConversation( currentUser, user ) )
+ {
+ throw new UpdateAccessDeniedException( "Not authorized to modify this object." );
+ }
+
+ Collection<MessageConversation> messageConversations = messageService.getMessageConversations( uids );
+
+ if ( messageConversations.isEmpty() )
+ {
+ response.setStatus( HttpServletResponse.SC_NOT_FOUND );
+ responseNode.addChild( new SimpleNode( "message", "No MessageConversations found for the given UIDs." ) );
+ return responseNode;
+ }
+
+ CollectionNode marked = responseNode.addChild( new CollectionNode( "markedUnread" ) );
+ marked.setWrapping( false );
+
+ for( MessageConversation conversation : messageConversations )
+ {
+ if( conversation.markUnread( user ) )
+ {
+ messageService.updateMessageConversation( conversation );
+ marked.addChild( new SimpleNode( "uid", conversation.getUid() ) );
+ }
+ }
+
+ response.setStatus( HttpServletResponse.SC_OK );
+
+ return responseNode;
+ }
+
+
+ //--------------------------------------------------------------------------
+ // Delete a MessageConversation (requires override auth)
+ //--------------------------------------------------------------------------
+
+ /**
+ * Deletes a MessageConversation.
+ * Note that this is a HARD delete and therefore requires override authority for the current user.
+ * @param uid the uid of the MessageConversation to delete.
+ * @throws Exception
+ */
+ @Override
+ @PreAuthorize( "hasRole('ALL') or hasRole('F_METADATA_IMPORT')" )
+ public void deleteObject( HttpServletResponse response, HttpServletRequest request, @PathVariable String uid )
+ throws Exception
+ {
+ super.deleteObject( response, request, uid );
+ }
+
+ //--------------------------------------------------------------------------
+ // Remove a user from a MessageConversation
+ // In practice a DELETE on MessageConversation <-> User relationship
+ //--------------------------------------------------------------------------
+
+ @RequestMapping( value = "/{mc-uid}/{user-uid}", method = RequestMethod.DELETE, produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE } )
+ public @ResponseBody RootNode removeUserFromMessageConversation(
+ @PathVariable( value = "mc-uid" ) String mcUid, @PathVariable( value = "user-uid" ) String userUid, HttpServletResponse response )
+ throws DeleteAccessDeniedException
+ {
+ RootNode responseNode = new RootNode( "reply" );
+
+ User user = userService.getUser( userUid );
+
+ if( user == null )
+ {
+ responseNode.addChild( new SimpleNode( "message", "No user with uid: " + userUid ) );
+ response.setStatus( HttpServletResponse.SC_NOT_FOUND );
+ return responseNode;
+ }
+
+ if( !canModifyUserConversation( currentUserService.getCurrentUser(), user ) )
+ {
+
+ throw new DeleteAccessDeniedException( "Not authorized to modify user: " + user.getUid() );
+ }
+
+ MessageConversation messageConversation = messageService.getMessageConversation( mcUid );
+
+ if( messageConversation == null )
+ {
+ responseNode.addChild( new SimpleNode( "message", "No messageConversation with uid: " + mcUid ) );
+ response.setStatus( HttpServletResponse.SC_NOT_FOUND );
+ return responseNode;
+ }
+
+ CollectionNode removed = responseNode.addChild( new CollectionNode( "removed" ) );
+
+ if( messageConversation.remove( user ) )
+ {
+ messageService.updateMessageConversation( messageConversation );
+ removed.addChild( new SimpleNode( "uid", messageConversation.getUid() ) );
+ }
+
+ response.setStatus( HttpServletResponse.SC_OK );
+
+ return responseNode;
+ }
+
+ //--------------------------------------------------------------------------
+ // Remove a user from one or more MessageConversations (batch operation)
+ //--------------------------------------------------------------------------
+
+ @RequestMapping( method = RequestMethod.DELETE, produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE } )
+ public @ResponseBody RootNode removeUserFromMessageConversations(
+ @RequestParam( "mc" ) String[] mcUids, @RequestParam( value = "user", required = false ) String userUid, HttpServletResponse response )
+ throws DeleteAccessDeniedException
+ {
+ RootNode responseNode = new RootNode( "response" );
+
+ User currentUser = currentUserService.getCurrentUser();
+
+ User user = userUid == null ? currentUser : userService.getUser( userUid ) ;
+
+ if( user == null )
+ {
+ response.setStatus( HttpServletResponse.SC_NOT_FOUND );
+ responseNode.addChild( new SimpleNode( "message", "User does not exist: " + userUid ) );
+ return responseNode;
+ }
+
+ if( !canModifyUserConversation( currentUser, user ) )
+ {
+ throw new DeleteAccessDeniedException( "Not authorized to modify user: " + user.getUid() );
+ }
+
+ Collection<MessageConversation> messageConversations = messageService.getMessageConversations( mcUids );
+
+ if( messageConversations.isEmpty() )
+ {
+ response.setStatus( HttpServletResponse.SC_NOT_FOUND );
+ responseNode.addChild( new SimpleNode( "message", "No MessageConversations found for the given UIDs." ) );
+ return responseNode;
+ }
+
+ CollectionNode removed = responseNode.addChild( new CollectionNode( "removed" ) );
+
+ for( MessageConversation mc : messageConversations )
+ {
+ if( mc.remove( user ) )
+ {
+ messageService.updateMessageConversation( mc );
+ removed.addChild( new SimpleNode( "uid", mc.getUid() ) );
+ }
+ }
+
+ response.setStatus( HttpServletResponse.SC_OK );
+
+ return responseNode;
+ }
+
+ //--------------------------------------------------------------------------
+ // Supportive methods
+ //--------------------------------------------------------------------------
+
+ /**
+ * Determines whether the current user has permission to modify the given user in a MessageConversation.
+ *
+ * The modification is either marking a conversation read/unread for the user or removing the user from the MessageConversation.
+ *
+ * Since there are no per-conversation authorities provided the permission is given if the current user equals the user
+ * or if the current user has update-permission to User objects.
+ *
+ * @param currentUser the current user to check authorization for.
+ * @param user the user to remove from a conversation.
+ * @return true if the current user is allowed to remove the user from a conversation, false otherwise.
+ */
+ private boolean canModifyUserConversation( User currentUser, User user )
+ {
+ return currentUser.equals( user ) || currentUser.getUserCredentials().hasAnyAuthority( AclService.ACL_OVERRIDE_AUTHORITIES );
+ }
+
+ /**
+ * Determines whether the given user has permission to read the MessageConversation.
+ *
+ * @param user the user to check permission for.
+ * @param messageConversation the MessageConversation to access.
+ * @return true if the user can read the MessageConversation, false otherwise.
+ */
+ private boolean canReadMessageConversation( User user, MessageConversation messageConversation )
+ {
+ return messageConversation.getUsers().contains( user ) || user.getUserCredentials().hasAnyAuthority( AclService.ACL_OVERRIDE_AUTHORITIES );
+ }
}
=== modified file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/css/widgets.css'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/css/widgets.css 2014-09-12 06:09:29 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/css/widgets.css 2014-09-22 12:02:33 +0000
@@ -742,6 +742,87 @@
}
/*----------------------------------------------------------------------------*/
+/* Multi select/checkbox combo button */
+/*----------------------------------------------------------------------------*/
+
+.multiSelectButton
+{
+ padding: 6px 12px;
+ height: 25px;
+ border: 1px solid #bbb;
+ border-radius: 3px;
+ margin-right: 4px;
+ font-weight: bold;
+ font-size: 13px;
+ background-color: #f3f3f3;
+ color: #606060 !important;
+ text-decoration: none !important;
+}
+
+.multiSelectButton:hover
+{
+ text-decoration: none;
+ background-color: #f8f8f8;
+}
+
+.multiSelectButton .downArrow
+{
+ border: thick;
+ border-color: #777777 transparent white;
+ border-style: solid dashed dashed;
+ margin-left: 5px;
+ position: relative;
+ top: 10px;
+}
+
+.multiSelectButton .downArrow:hover
+{
+ border-color: #999999 transparent white;
+}
+
+.multiSelectButton.disabled .downArrow
+{
+ border-color: #e2e2e2 transparent white;
+}
+
+.multiSelectButton input[type='checkbox']
+{
+ margin: 0;
+}
+
+.multiSelectMenu
+{
+ position: absolute;
+ margin-top: -15px;
+ z-index: 999;
+ padding: 1px 1px;
+ border: 1px solid #bbb;
+ border-radius: 3px;
+ margin-right: 4px;
+ font-weight: bold;
+ font-size: 13px;
+ background-color: #f3f3f3;
+ color: #606060 !important;
+ text-decoration: none !important;
+}
+
+.multiSelectMenu li
+{
+ float: none !important;
+ display: block;
+ text-decoration: none;
+ color: #606060;
+ padding: 6px 9px;
+}
+
+.multiSelectMenu li:hover
+{
+ cursor: pointer;
+ text-decoration: none;
+ background-color: #f8f8f8;
+}
+
+/*----------------------------------------------------------------------------*/
/* Placeholders */
/*----------------------------------------------------------------------------*/
=== added file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/jQuery/jquery.dhisCheckboxMenu.js'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/jQuery/jquery.dhisCheckboxMenu.js 1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/jQuery/jquery.dhisCheckboxMenu.js 2014-09-22 15:24:24 +0000
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2004-2013, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * Checkbox/dropdown combo menu for DHIS 2 Dashboard.
+ *
+ * @author Halvdan Hoem Grelland
+ */
+( function ( $ ) {
+ /*
+ Example markup:
+ <div>
+ <div></div> <!-- Empty, will be filled with button markup -->
+ <ul> <!-- Menu markup -->
+ <li data-action="actionA">Item A</li>
+ <li data-action="actionB">Item B</li>
+ <li data-action="actionC">Item C</li>
+ </ul>
+ </div>
+ ...
+ <div id="checkboxes">
+ ...
+ <input type="checkbox" value="someValue" .... />
+ ...
+ </div>
+
+ The string parameter given in data-action denotes the name of the
+ function which is called on click of the menu item.
+
+ The selected checkboxes' values will be given as an array argument
+ to the function when called.
+
+ Usage:
+ $( "#myDiv" ).multiCheckboxMenu( $( "#myCheckboxContainer" ), {} );
+
+ */
+
+ function getCheckedValues( $checkboxContainer ) {
+ var checked = [];
+ $checkboxContainer.find( "input:checkbox:checked" ).each( function() {
+ checked.push( this.value );
+ });
+ return checked;
+ }
+
+ var multiCheckboxMenu = $.fn.multiCheckboxMenu;
+
+ $.fn.multiCheckboxMenu = function( $checkboxContainer, options ) {
+
+ if( typeof options !== "object" ) {
+ options = {};
+ }
+
+ var $cb = $( "<input>", { type: "checkbox" } );
+ options = $.extend( true, options , {
+ checkbox: $cb,
+ buttonElements: [
+ $( "<span>", { "class": "downArrow" } )
+ ],
+ menuClass: "multiSelectMenu",
+ buttonClass: "multiSelectButton"
+ });
+
+ var $checkbox = $( options.checkbox );
+ var $slaveCheckboxes = $checkboxContainer.find( "input:checkbox" );
+
+ var $button = $( "<a>", { href: "#" } );
+ $button.addClass( options.buttonClass );
+
+ $button.append( $( options.checkbox ) );
+
+ $( options.buttonElements ).each( function() {
+ $button.append( $( this ) );
+ });
+
+ $( this ).find( "div:first" ).append($button);
+
+ var $menu = $( this ).find( "ul" );
+ $menu.addClass( options.menuClass );
+ $menu.css( "visibility", "hidden" );
+ $menu.position({
+ my: "left top",
+ at: "left bottom",
+ of: $button
+ });
+
+ $button.click( function ( event ) {
+ $( document ).one( "click", function() {
+ $menu.css( "visibility", "hidden" );
+ });
+
+ if( $menu.css( "visibility" ) !== "visible" )
+ {
+ $menu.css( "visibility", "visible" );
+ }
+ else
+ {
+ $menu.css( "visibility", "hidden" );
+ }
+ event.stopPropagation();
+ });
+
+ $menu.find( "li" ).each( function() {
+ var el = $( this );
+ el.action = this.getAttribute( "data-action" );
+
+ if( typeof el.action === "undefined" )
+ {
+ el.action = function(){};
+ }
+
+ el.click( function() {
+ var checked = getCheckedValues( $checkboxContainer );
+
+ $checkbox.removeAttr( "checked" );
+ $slaveCheckboxes.removeAttr( "checked" );
+
+ return window[ el.action ]( checked );
+ });
+ });
+
+ $checkbox.click( function( event ) {
+ if( this.checked )
+ {
+ $slaveCheckboxes.attr( "checked", "checked" );
+ }
+ else
+ {
+ $slaveCheckboxes.removeAttr( "checked" );
+ }
+ event.stopPropagation();
+ });
+
+ $slaveCheckboxes.click( function() {
+ var checked = $slaveCheckboxes.filter( ":checked" );
+
+ if( checked.length < 1 )
+ {
+ $checkbox.removeAttr( "checked" );
+ }
+ else if( checked.length > 0 && checked.length < $slaveCheckboxes.length )
+ {
+ $checkbox.removeAttr( "checked" );
+ }
+ else
+ {
+ $checkbox.attr( "checked", "checked" );
+ }
+ });
+ };
+})( jQuery );
=== modified file 'dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/java/org/hisp/dhis/dashboard/message/action/ReadMessageAction.java'
--- dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/java/org/hisp/dhis/dashboard/message/action/ReadMessageAction.java 2014-03-18 08:10:10 +0000
+++ dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/java/org/hisp/dhis/dashboard/message/action/ReadMessageAction.java 2014-09-01 14:56:40 +0000
@@ -89,9 +89,19 @@
public String execute()
throws Exception
{
+ if( id == null )
+ {
+ return ERROR;
+ }
+
User user = currentUserService.getCurrentUser();
-
+
conversation = messageService.getMessageConversation( id );
+
+ if( conversation == null )
+ {
+ return ERROR;
+ }
if ( conversation.markRead( user ) )
{
=== modified file 'dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/resources/org/hisp/dhis/dashboard/i18n_module.properties'
--- dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/resources/org/hisp/dhis/dashboard/i18n_module.properties 2013-12-02 13:59:00 +0000
+++ dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/resources/org/hisp/dhis/dashboard/i18n_module.properties 2014-08-11 14:16:43 +0000
@@ -25,10 +25,15 @@
write_new_feedback=Write new feedback
recipients=Recipients
mark_unread=Mark as unread
+mark_read=Mark as read
read=Read
+delete=Delete
confirm_delete_message=Are you sure you want to delete the message?
+confirm_delete_all_selected_messages=Are you sure you want to delete all selected messages?
+no_messages_selected=No messages selected
unread_messages=unread messages
unread_message=unread message
+messages_were_deleted=Messages were deleted
discard=Discard
enter_subject=Please enter a subject
enter_text=Please enter text
=== modified file 'dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/resources/struts.xml'
--- dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/resources/struts.xml 2013-08-18 18:54:32 +0000
+++ dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/resources/struts.xml 2014-09-22 13:49:28 +0000
@@ -26,7 +26,7 @@
<result name="success" type="velocity">/main.vm</result>
<param name="page">/dhis-web-dashboard-integration/message.vm</param>
<param name="menu">/dhis-web-commons/about/menuDashboard.vm</param>
- <param name="javascripts">javascript/message.js</param>
+ <param name="javascripts">javascript/message.js,../dhis-web-commons/javascripts/jQuery/jquery.dhisCheckboxMenu.js</param>
<param name="stylesheets">style/dashboard.css</param>
</action>
=== modified file 'dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/webapp/dhis-web-dashboard-integration/javascript/message.js'
--- dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/webapp/dhis-web-dashboard-integration/javascript/message.js 2012-10-12 15:48:35 +0000
+++ dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/webapp/dhis-web-dashboard-integration/javascript/message.js 2014-09-22 13:49:28 +0000
@@ -1,7 +1,7 @@
function submitMessage()
{
- $( "#messageForm" ).submit();
+ $( "#messageForm" ).submit();
}
function removeMessage( id )
@@ -9,6 +9,102 @@
removeItem( id, "", i18n_confirm_delete_message, "removeMessage.action" );
}
+function removeMessages( messages )
+{
+ if( typeof messages === "undefined" || messages.length < 1 )
+ {
+ return;
+ }
+
+ var confirmed = window.confirm( i18n_confirm_delete_all_selected_messages );
+
+ if ( confirmed )
+ {
+ setHeaderWaitMessage( i18n_deleting );
+
+ $.ajax(
+ {
+ url: "../../api/messageConversations?" + $.param( { mc: messages }, true ),
+ contentType: "application/json",
+ dataType: "json",
+ type: "DELETE",
+ success: function( response )
+ {
+ for( var i = 0 ; i < response.removed.length ; i++ )
+ {
+ $( "#messages" ).find( "[name='" + response.removed[i] + "']" ).remove();
+ }
+ setHeaderDelayMessage( i18n_messages_were_deleted );
+ },
+ error: function( response )
+ {
+ showErrorMessage( response.message, 3 );
+ }
+ });
+ }
+}
+
+function markMessagesRead( messages )
+{
+ if( messages.length < 1 )
+ {
+ return;
+ }
+
+ $.ajax(
+ {
+ url: "../../api/messageConversations/read",
+ type: "PUT",
+ data: JSON.stringify( messages ),
+ contentType: "application/json",
+ dataType: "json",
+ success: function( response )
+ {
+ toggleMessagesRead( response.markedRead );
+ },
+ error: function( response )
+ {
+ showErrorMessage( response.message, 3 );
+ }
+ });
+}
+
+function markMessagesUnread( messages )
+{
+ if( messages.length < 1 )
+ {
+ return;
+ }
+
+ $.ajax(
+ {
+ url: "../../api/messageConversations/unread",
+ type: "PUT",
+ data: JSON.stringify( messages ),
+ contentType: "application/json",
+ dataType: "json",
+ success: function( response )
+ {
+ toggleMessagesRead( response.markedUnread );
+ },
+ error: function( response )
+ {
+ showErrorMessage( response.message, 3 );
+ }
+ });
+}
+
+function toggleMessagesRead( messageUids )
+{
+ var messages = $( "#messages" );
+
+ for( var i = 0 ; i < messageUids.length ; i++ )
+ {
+ messages.find( "[name='" + messageUids[i] + "']" ).toggleClass( "unread bold" );
+ messages.find( "input:checkbox" ).removeAttr( "checked" );
+ }
+}
+
function read( id )
{
window.location.href = "readMessage.action?id=" + id;
=== modified file 'dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/webapp/dhis-web-dashboard-integration/message.vm'
--- dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/webapp/dhis-web-dashboard-integration/message.vm 2013-10-28 13:30:18 +0000
+++ dhis-2/dhis-web/dhis-web-dashboard-integration/src/main/webapp/dhis-web-dashboard-integration/message.vm 2014-09-22 15:08:21 +0000
@@ -1,50 +1,77 @@
-
<h3>$i18n.getString( "messages" ) #openHelp( "dashboard_messages" )</h3>
<div class="horizontalMenu" style="padding: 8px 0 40px 0;">
<ul>
- #if( $auth.hasAccess( "dhis-web-dashboard-integration", "sendMessage" ) )
+
+ #if( $auth.hasAccess( "dhis-web-dashboard-integration", "sendMessage" ) )
<li><a class="blueButtonLink" href="showSendMessage.action">$i18n.getString( 'write_message' )</a></li>
#end
<li><a class="blueButtonLink" href="showSendFeedback.action">$i18n.getString( "write_feedback" )</a></li>
<li><span style="padding-left:12px"></span></li>
+ <li>
+ <div id="checkboxDropdown">
+ <div></div>
+ <ul>
+ <li data-action="removeMessages">Delete</li>
+ <li data-action="markMessagesRead">Mark read</li>
+ <li data-action="markMessagesUnread">Mark unread</li>
+ </ul>
+ </div>
+ </li>
+ <li><span style="padding-left:12px"></span></li>
<li><a class="greyButtonLink" href="message.action">$i18n.getString( "inbox" )</a></li>
<li><a class="greyButtonLink" href="message.action?followUp=true">$i18n.getString( "follow_up" )</a></li>
<li><a class="greyButtonLink" href="message.action?unread=true">$i18n.getString( "unread" )</a></li>
</ul>
</div>
-
<div style="width:100%">
-<div style="padding-right:15px">
-<table class="plainList" style="width:100%">
- <tr>
- <th></th>
- <th>$i18n.getString( "sender" )</th>
- <th>$i18n.getString( "subject" )</th>
- <th>$i18n.getString( "date" )</th>
- <th></th>
- </tr>
- #foreach( $conversation in $conversations )
- <tr id="tr${conversation.id}" #if( !$conversation.read )class="unread bold"#end>
- <td style="width:40px;padding-left:5px;" onclick="toggleFollowUp( '${conversation.id}' )">
- <img id="followUp${conversation.id}" #if( $conversation.followUp ) src="../images/marked.png"#else src="../images/unmarked.png"#end></td>
- <td style="width:200px" onclick="read( '${conversation.uid}' )">
- #if( $conversation.lastSenderName )$!encoder.htmlEncode( $conversation.lastSenderName )#else$i18n.getString( "system_notification" )#end
- #if( $conversation.messageCount > 1 ) <span class="normal">(${conversation.messageCount})</span>#end
- </td>
- <td onclick="read( '${conversation.uid}' )">$!encoder.htmlEncode( $conversation.subject )</td>
- <td onclick="read( '${conversation.uid}' )" style="width:80px">$!format.formatDate( $conversation.lastMessage )</td>
- <td style="width:70px; text-align:center;">
- <a href="readMessage.action?id=${conversation.uid}"><img src="../images/read.png" title="$i18n.getString( 'read' )"></a>
- <a href="javascript:removeMessage( '${conversation.id}' )"><img src="../images/delete.png" title="$i18n.getString( 'delete' )"></a>
- </td>
- </tr>
- #end
-</table>
-#parse( "/dhis-web-commons/paging/paging.vm" )
-</div>
-</div>
-
+ <div style="padding-right:15px">
+ <table class="plainList" style="width:100%">
+ <thead>
+ <tr>
+ <th></th>
+ <th></th>
+ <th>$i18n.getString( "sender" )</th>
+ <th>$i18n.getString( "subject" )</th>
+ <th>$i18n.getString( "date" )</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody id="messages" >
+ #foreach( $conversation in $conversations )
+ <tr name="${conversation.uid}" id="tr${conversation.id}" #if( !$conversation.read )class="unread bold"#end>
+ <td style="width:20px;padding-left:5px;">
+ <input type="checkbox" value="${conversation.uid}" />
+ </td>
+ <td style="width:40px;padding-left:5px;" onclick="toggleFollowUp( '${conversation.id}' )">
+ <img id="followUp${conversation.id}" #if( $conversation.followUp ) src="../images/marked.png"#else src="../images/unmarked.png"#end></td>
+ <td style="width:200px" onclick="read( '${conversation.uid}' )">
+ #if( $conversation.lastSenderName )$!encoder.htmlEncode( $conversation.lastSenderName )#else$i18n.getString( "system_notification" )#end
+ #if( $conversation.messageCount > 1 ) <span class="normal">(${conversation.messageCount})</span>#end
+ </td>
+ <td onclick="read( '${conversation.uid}' )">$!encoder.htmlEncode( $conversation.subject )</td>
+ <td onclick="read( '${conversation.uid}' )" style="width:80px">$!format.formatDate( $conversation.lastMessage )</td>
+ <td style="width:70px; text-align:center;">
+ <a href="readMessage.action?id=${conversation.uid}"><img src="../images/read.png" title="$i18n.getString( 'read' )"></a>
+ <a href="javascript:removeMessage( '${conversation.id}' )"><img src="../images/delete.png" title="$i18n.getString( 'delete' )"></a>
+ </td>
+ </tr>
+ #end
+ </tbody>
+ </table>
+ #parse( "/dhis-web-commons/paging/paging.vm" )
+ </div>
+</div>
<script type="text/javascript">
- var i18n_confirm_delete_message = '$encoder.jsEscape( $i18n.getString( "confirm_delete_message" ) , "'" )';
+$( document ).ready( function() {
+ i18n_confirm_delete_message = '$encoder.jsEscape( $i18n.getString( "confirm_delete_message" ) , "'" )';
+ i18n_confirm_delete_all_selected_messages = '$encoder.jsEscape( $i18n.getString( "confirm_delete_all_selected_messages" ), "'" )';
+ i18n_no_messages_selected = '$encoder.jsEscape( $i18n.getString( "no_messages_selected" ), "'" )';
+ i18n_messages_were_deleted = '$encoder.jsEscape( $i18n.getString( "messages_were_deleted" ), "'" )';
+ i18n_delete = '$encoder.jsEscape( $i18n.getString( "delete" ), "'" )';
+ i18n_mark_read = '$encoder.jsEscape( $i18n.getString( "mark_read" ), "'" )';
+ i18n_mark_unread = '$encoder.jsEscape( $i18n.getString( "mark_unread" ) , "'" )';
+
+ $( "#checkboxDropdown" ).multiCheckboxMenu( $( "#messages" ) );
+});
</script>