=== 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 iterator = userMessages.iterator(); @@ -225,9 +225,10 @@ { iterator.remove(); - return; + return true; } } + return false; } public Set 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 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 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 getMessageConversations( boolean followUpOnly, boolean unreadOnly, int first, int max ); + Collection 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 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 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 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 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 getEntityList( WebMetaData metaData, WebOptions options ) { List 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 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 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 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: +
+
+
    +
  • Item A
  • +
  • Item B
  • +
  • Item C
  • +
+
+ ... +
+ ... + + ... +
+ + 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 = $( "", { type: "checkbox" } ); + options = $.extend( true, options , { + checkbox: $cb, + buttonElements: [ + $( "", { "class": "downArrow" } ) + ], + menuClass: "multiSelectMenu", + buttonClass: "multiSelectButton" + }); + + var $checkbox = $( options.checkbox ); + var $slaveCheckboxes = $checkboxContainer.find( "input:checkbox" ); + + var $button = $( "", { 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 @@ /main.vm /dhis-web-dashboard-integration/message.vm /dhis-web-commons/about/menuDashboard.vm - javascript/message.js + javascript/message.js,../dhis-web-commons/javascripts/jQuery/jquery.dhisCheckboxMenu.js style/dashboard.css === 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 @@ -

$i18n.getString( "messages" ) #openHelp( "dashboard_messages" )

-
-
- - - - - - - - - #foreach( $conversation in $conversations ) - - - - - - - - #end -
$i18n.getString( "sender" )$i18n.getString( "subject" )$i18n.getString( "date" )
- - #if( $conversation.lastSenderName )$!encoder.htmlEncode( $conversation.lastSenderName )#else$i18n.getString( "system_notification" )#end - #if( $conversation.messageCount > 1 ) (${conversation.messageCount})#end - $!encoder.htmlEncode( $conversation.subject )$!format.formatDate( $conversation.lastMessage ) - - -
-#parse( "/dhis-web-commons/paging/paging.vm" ) -
-
- +
+ + + + + + + + + + + + + #foreach( $conversation in $conversations ) + + + + + + + + + #end + +
$i18n.getString( "sender" )$i18n.getString( "subject" )$i18n.getString( "date" )
+ + + + #if( $conversation.lastSenderName )$!encoder.htmlEncode( $conversation.lastSenderName )#else$i18n.getString( "system_notification" )#end + #if( $conversation.messageCount > 1 ) (${conversation.messageCount})#end + $!encoder.htmlEncode( $conversation.subject )$!format.formatDate( $conversation.lastMessage ) + + +
+ #parse( "/dhis-web-commons/paging/paging.vm" ) +
+