← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 14686: wip, new selected/available plugin, only used in user add/edit for userGroups for now

 

------------------------------------------------------------
revno: 14686
committer: Morten Olav Hansen <mortenoh@xxxxxxxxx>
branch nick: dhis2
timestamp: Sat 2014-04-05 13:00:20 +0700
message:
  wip, new selected/available plugin, only used in user add/edit for userGroups for now
added:
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/javascript/jqSelected.js
modified:
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/SetupTreeAction.java
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/UpdateUserAction.java
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/org/hisp/dhis/user/i18n_module.properties
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/addUserForm.vm
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/updateUserForm.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-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java	2014-03-23 18:26:50 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java	2014-04-05 06:00:20 +0000
@@ -28,14 +28,8 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import javax.servlet.http.HttpServletRequest;
-
+import com.google.common.collect.Lists;
+import com.opensymphony.xwork2.Action;
 import org.apache.struts2.ServletActionContext;
 import org.hisp.dhis.api.utils.ContextUtils;
 import org.hisp.dhis.attribute.AttributeService;
@@ -50,18 +44,26 @@
 import org.hisp.dhis.user.User;
 import org.hisp.dhis.user.UserAuthorityGroup;
 import org.hisp.dhis.user.UserCredentials;
+import org.hisp.dhis.user.UserGroup;
+import org.hisp.dhis.user.UserGroupService;
 import org.hisp.dhis.user.UserService;
 import org.hisp.dhis.user.UserSetting;
 import org.hisp.dhis.user.UserSettingService;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.StringUtils;
 
-import com.opensymphony.xwork2.Action;
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 /**
  * @author Torgeir Lorange Ostby
  */
 public class AddUserAction
-        implements Action
+    implements Action
 {
     private String ACCOUNT_ACTION_INVITE = "invite";
 
@@ -90,6 +92,9 @@
         this.userService = userService;
     }
 
+    @Autowired
+    private UserGroupService userGroupService;
+
     private SecurityService securityService;
 
     public void setSecurityService( SecurityService securityService )
@@ -218,6 +223,13 @@
         this.selectedList = selectedList;
     }
 
+    private List<String> ugSelected = Lists.newArrayList();
+
+    public void setUgSelected( List<String> ugSelected )
+    {
+        this.ugSelected = ugSelected;
+    }
+
     private List<String> jsonAttributeValues;
 
     public void setJsonAttributeValues( List<String> jsonAttributeValues )
@@ -230,7 +242,7 @@
     // -------------------------------------------------------------------------
 
     public String execute()
-            throws Exception
+        throws Exception
     {
         // ---------------------------------------------------------------------
         // Prepare values
@@ -269,7 +281,7 @@
             userCredentials.setUsername( inviteUsername );
             user.setEmail( inviteEmail );
 
-            securityService.prepareUserForInvite ( userCredentials );
+            securityService.prepareUserForInvite( userCredentials );
         }
         else
         {
@@ -284,20 +296,20 @@
         user.updateOrganisationUnits( new HashSet<OrganisationUnit>( orgUnits ) );
 
         Set<UserAuthorityGroup> userAuthorityGroups = new HashSet<UserAuthorityGroup>();
-        
+
         for ( String id : selectedList )
         {
             userAuthorityGroups.add( userService.getUserAuthorityGroup( Integer.parseInt( id ) ) );
         }
 
         userService.canIssueFilter( userAuthorityGroups );
-        
+
         userCredentials.setUserAuthorityGroups( userAuthorityGroups );
 
         if ( jsonAttributeValues != null )
         {
             AttributeUtils.updateAttributeValuesFromJson( user.getAttributeValues(), jsonAttributeValues,
-                    attributeService );
+                attributeService );
         }
 
         userService.addUser( user );
@@ -318,6 +330,17 @@
             securityService.sendRestoreMessage( userCredentials, getRootPath(), restoreOptions );
         }
 
+        for ( String id : ugSelected )
+        {
+            UserGroup userGroup = userGroupService.getUserGroup( id );
+
+            if ( userGroup != null )
+            {
+                userGroup.addUser( user );
+                userGroupService.updateUserGroup( userGroup );
+            }
+        }
+
         return SUCCESS;
     }
 

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/SetupTreeAction.java'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/SetupTreeAction.java	2014-03-23 18:26:50 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/SetupTreeAction.java	2014-04-05 06:00:20 +0000
@@ -126,6 +126,13 @@
         return userCredentials;
     }
 
+    private User user;
+
+    public User getUser()
+    {
+        return user;
+    }
+
     private List<UserAuthorityGroup> userAuthorityGroups;
 
     public List<UserAuthorityGroup> getUserAuthorityGroups()
@@ -199,7 +206,7 @@
         
         if ( id != null )
         {
-            User user = userService.getUser( id );
+            user = userService.getUser( id );
 
             if ( user.getOrganisationUnits().size() > 0 )
             {

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/UpdateUserAction.java'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/UpdateUserAction.java	2014-03-23 18:26:50 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/UpdateUserAction.java	2014-04-05 06:00:20 +0000
@@ -28,12 +28,9 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.opensymphony.xwork2.Action;
 import org.hisp.dhis.attribute.AttributeService;
 import org.hisp.dhis.organisationunit.OrganisationUnit;
 import org.hisp.dhis.oust.manager.SelectionTreeManager;
@@ -45,12 +42,19 @@
 import org.hisp.dhis.user.User;
 import org.hisp.dhis.user.UserAuthorityGroup;
 import org.hisp.dhis.user.UserCredentials;
+import org.hisp.dhis.user.UserGroup;
+import org.hisp.dhis.user.UserGroupService;
 import org.hisp.dhis.user.UserService;
 import org.hisp.dhis.user.UserSetting;
 import org.hisp.dhis.user.UserSettingService;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.StringUtils;
 
-import com.opensymphony.xwork2.Action;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 /**
  * @author Torgeir Lorange Ostby
@@ -69,6 +73,9 @@
         this.userService = userService;
     }
 
+    @Autowired
+    private UserGroupService userGroupService;
+
     private PasswordManager passwordManager;
 
     public void setPasswordManager( PasswordManager passwordManager )
@@ -178,6 +185,13 @@
         this.selectedList = selectedList;
     }
 
+    private List<String> ugSelected = Lists.newArrayList();
+
+    public void setUgSelected( List<String> ugSelected )
+    {
+        this.ugSelected = ugSelected;
+    }
+
     private List<String> jsonAttributeValues;
 
     public void setJsonAttributeValues( List<String> jsonAttributeValues )
@@ -238,7 +252,7 @@
         }
 
         userService.canIssueFilter( userAuthorityGroups );
-        
+
         userCredentials.setUserAuthorityGroups( userAuthorityGroups );
 
         if ( rawPassword != null )
@@ -272,6 +286,33 @@
         userService.addOrUpdateUserSetting( new UserSetting( user, UserSettingService.KEY_UI_LOCALE, LocaleUtils.getLocale( localeUi ) ) );
         userService.addOrUpdateUserSetting( new UserSetting( user, UserSettingService.KEY_DB_LOCALE, LocaleUtils.getLocale( localeDb ) ) );
 
+        Set<UserGroup> userGroups = Sets.newHashSet();
+
+        for ( String id : ugSelected )
+        {
+            UserGroup userGroup = userGroupService.getUserGroup( id );
+
+            if ( userGroup != null )
+            {
+                userGroups.add( userGroup );
+            }
+        }
+
+        for ( UserGroup userGroup : Sets.newHashSet( user.getGroups() ) )
+        {
+            if ( !userGroups.contains( userGroup ) )
+            {
+                userGroup.removeUser( user );
+                userGroupService.updateUserGroup( userGroup );
+            }
+        }
+
+        for ( UserGroup userGroup : userGroups )
+        {
+            userGroup.addUser( user );
+            userGroupService.updateUserGroup( userGroup );
+        }
+
         return SUCCESS;
     }
 }

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/org/hisp/dhis/user/i18n_module.properties'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/org/hisp/dhis/user/i18n_module.properties	2014-03-28 01:53:11 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/org/hisp/dhis/user/i18n_module.properties	2014-04-05 06:00:20 +0000
@@ -290,6 +290,8 @@
 unit=Unit
 selected_roles=Selected roles
 available_roles=Available roles
+selected_user_groups=Selected User Groups
+available_user_groups=Available User Groups
 create_new_user=Create new user
 password=Password
 firstName=First name

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/addUserForm.vm'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/addUserForm.vm	2014-03-05 05:52:03 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/addUserForm.vm	2014-04-05 06:00:20 +0000
@@ -1,9 +1,11 @@
+<script src="javascript/jqSelected.js"></script>
 <script type="text/javascript">
 	jQuery(function()
     {
 	    validation2( 'addUserForm', function( form )
 	    {
-	        jQuery( "#selectedList" ).children().attr( "selected", true );
+	        jQuery( "#selectedList" ).children().attr( "selected", "selected" );
+	        jQuery( "#ugSelected" ).children().attr( "selected", "selected" );
 
             if( $('#selectionTree').find('.selected').size() == 0 ) {
                 setHeaderDelayMessage('$encoder.jsEncode( $i18n.getString( "organisation_unit_required_for_user" ) )');
@@ -141,6 +143,43 @@
 
 #tblDynamicAttributes( { "attributes": $attributes } )
 
+<!-- will be replaced by velocity macro -->
+<table>
+<colgroup>
+  <col style="width: 500px;"/>
+  <col/>
+  <col style="width: 500px;"/>
+</colgroup>
+<thead>
+<tr>
+  <th>$i18n.getString( "available_user_groups" )</th>
+  <th></th>
+  <th>$i18n.getString( "selected_user_groups" )</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+  <td><input id="ugAvailableSearch" type="text" placeholder=" Search.." style="width: 100%; margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0;" /></td>
+  <td></td>
+  <td></td>
+</tr>
+<tr>
+  <td>
+    <select id="ugAvailable" name="ugAvailable" multiple="multiple" style="height: 200px; width: 100%;"/>
+  </td>
+  <td>
+    <button type="button" style="width: 25px; text-align: center;" onclick="$.fn.selected.defaults.defaultMoveSelected('#ugAvailable');"><span class="fa fa-angle-right"> </span></button><br />
+    <button type="button" style="width: 25px; text-align: center;" onclick="$.fn.selected.defaults.defaultMoveSelected('#ugSelected');"><span class="fa fa-angle-left"> </span></button><br />
+    <button type="button" style="width: 25px; text-align: center;" onclick="$.fn.selected.defaults.defaultMoveAll('#ugAvailable');"><span class="fa fa-angle-double-right"> </span></button><br />
+    <button type="button" style="width: 25px; text-align: center;" onclick="$.fn.selected.defaults.defaultMoveAll('#ugSelected');"><span class="fa fa-angle-double-left"> </span></button><br />
+  </td>
+  <td>
+    <select id="ugSelected" name="ugSelected" multiple="multiple" style="height: 200px; width: 100%;" />
+  </td>
+</tr>
+</tbody>
+</table>
+
 <table>
     <colgroup>
       <col style="width: 500px;"/>

=== added file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/javascript/jqSelected.js'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/javascript/jqSelected.js	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/javascript/jqSelected.js	2014-04-05 06:00:20 +0000
@@ -0,0 +1,161 @@
+/**
+ * @author Morten Olav Hansen <mortenoh@xxxxxxxxx>
+ */
+
+$(function() {
+  var $ugTarget = $('#ugSelected');
+  var $ugAvailableSearch = $('#ugAvailableSearch');
+
+  $('#ugAvailable').selected({
+    url: '../api/userGroups.json',
+    target: $ugTarget,
+    search: $ugAvailableSearch,
+    iterator: 'userGroups'
+  });
+});
+
+!(function( $, window, document, undefined ) {
+  var methods = {
+    create: function( options ) {
+      var settings = {};
+      $.extend(settings, $.fn.selected.defaults, options);
+
+      if( settings.target === undefined ) {
+        $.error('selected: Missing options.target, please add your target box either as a jqEl or as a query.');
+      } else if( settings.url === undefined ) {
+        $.error('selected: Missing options.url, please give URL of where to find the source data.');
+      } else if( !$.isFunction(settings.handler) ) {
+        $.error('selected: Invalid options.handler.');
+      }
+
+      // pass-through if jqEl, query if string
+      settings.source = this;
+      settings.target = $(settings.target);
+      settings.search = $(settings.search);
+
+      if( !(settings.source instanceof $) ) {
+        $.error('selected: Invalid source.');
+      } else if( !(settings.target instanceof $) ) {
+        $.error('selected: Invalid target.');
+      }
+
+      settings.source.data('selected', settings);
+      settings.target.data('selected', settings);
+
+      settings.page = 1;
+      settings.defaultProgressiveLoader(settings);
+
+      settings.source.on('dblclick', 'option', settings.defaultSourceDblClickHandler);
+      settings.target.on('dblclick', 'option', settings.defaultTargetDblClickHandler);
+      settings.source.on('scroll', settings.makeScrollHandler(settings));
+
+      if( settings.search instanceof $ ) {
+        settings.search.on('keypress', settings.makeSearchHandler(settings));
+      }
+    }
+  };
+
+  methods.defaultMethod = methods.create;
+
+  // method dispatcher
+  $.fn.selected = function( method ) {
+    var args = Array.prototype.slice.call(arguments, 1);
+
+    if( $.isFunction(methods[method]) ) {
+      return methods[method].apply(this, args);
+    } else if( $.isPlainObject(method) || $.type(method) === 'undefined' ) {
+      return methods.defaultMethod.apply(this, arguments);
+    } else {
+      $.error('selected: Unknown method');
+    }
+  };
+
+  $.fn.selected.defaults = {
+    iterator: 'objects',
+    handler: function( item ) {
+      return $('<option/>').val(item.id).text(item.name);
+    },
+    defaultMoveSelected: function( sel ) {
+      $(sel).find(':selected').trigger('dblclick');
+    },
+    defaultMoveAll: function( sel ) {
+      $(sel).find('option').attr('selected', 'selected').trigger('dblclick');
+    },
+    defaultSourceDblClickHandler: function() {
+      var $this = $(this);
+      var $selected = $this.parent().data('selected');
+
+      if( $selected === undefined ) {
+        $.error('selected: Invalid source.parent, does not contain selected object.');
+      }
+
+      $this.removeAttr('selected');
+      $selected.target.append($this);
+    },
+    defaultTargetDblClickHandler: function() {
+      var $this = $(this);
+      var $selected = $this.parent().data('selected');
+
+      if( $selected === undefined ) {
+        $.error('selected: Invalid target.parent, does not contain selected object.');
+      }
+
+      $this.removeAttr('selected');
+      $selected.source.append($this);
+    },
+    makeSearchHandler: function( settings ) {
+      return function( e ) {
+        if( e.keyCode == 13 ) {
+          settings.defaultProgressiveLoader(settings, $(this).val());
+          e.preventDefault();
+        }
+      }
+    },
+    makeScrollHandler: function( settings ) {
+      return function( e ) {
+        if( settings.source[0].offsetHeight + settings.source.scrollTop() >= settings.source[0].scrollHeight ) {
+          settings.defaultProgressiveLoader(settings);
+        }
+      }
+    },
+    defaultProgressiveLoader: function( settings, search ) {
+      var request = {
+        url: settings.url,
+        data: {
+          paging: true,
+          pageSize: 50,
+          page: settings.page
+        },
+        dataType: 'json'
+      };
+
+      if( search !== undefined && search.length > 0 ) {
+        request.data.filter = 'name:like:' + search;
+      }
+
+      return $.ajax(request).done(function( data ) {
+        if( data.pager === undefined || data.pager.page == 1 ) {
+          settings.page = 1;
+          settings.source.children().remove();
+        }
+
+        if( data.pager.pageCount >= settings.page ) {
+          settings.page = data.pager.pageCount;
+        }
+
+        if( data[settings.iterator] === undefined ) {
+          $.error('selected: Invalid iterator for source url: ' + settings.iterator);
+        }
+
+        $.each(data[settings.iterator], function( idx ) {
+          if( settings.target.find('option[value=' + this.id + ']').length == 0 ) {
+            settings.source.append(settings.handler(this));
+          }
+        });
+      }).fail(function() {
+        settings.source.children().remove();
+      });
+    }
+  };
+
+})(jQuery, window, document);

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/updateUserForm.vm'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/updateUserForm.vm	2014-03-05 05:52:03 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/updateUserForm.vm	2014-04-05 06:00:20 +0000
@@ -1,3 +1,4 @@
+<script src="javascript/jqSelected.js"></script>
 <script type="text/javascript">
 	jQuery(function() {
 	    var rules = getValidationRules( "user" );
@@ -7,7 +8,8 @@
 
 	    validation2( 'updateUserForm', function( form )
 	    {
-	        jQuery( "#selectedList" ).children().attr( "selected", true );
+            jQuery( "#selectedList" ).children().attr( "selected", "selected" );
+ 	        jQuery( "#ugSelected" ).children().attr( "selected", "selected" );
 
             if( $('#selectionTree').find('.selected').size() == 0 ) {
                 setHeaderDelayMessage('$i18n.getString( "organisation_unit_required_for_user" )');
@@ -48,10 +50,10 @@
 <h3>$i18n.getString( "edit_user" )</h3>
 
 <form id="updateUserForm" action="updateUser.action" method="post" class="inputForm">
-	
-  <div>
-	<input type="hidden" id="id" name="id" value="$userCredentials.id"/>
-  </div>
+
+<div>
+    <input type="hidden" id="id" name="id" value="$userCredentials.id"/>
+</div>
 
 <table>
     <col style="width: 120px"/>
@@ -130,6 +132,47 @@
 
 #tblDynamicAttributes( { "attributes": $attributes, "attributeValues": $attributeValues } )
 
+<!-- will be replaced by velocity macro -->
+<table>
+<colgroup>
+<col style="width: 500px;"/>
+<col/>
+<col style="width: 500px;"/>
+</colgroup>
+<thead>
+<tr>
+<th>$i18n.getString( "available_user_groups" )</th>
+<th></th>
+<th>$i18n.getString( "selected_user_groups" )</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td><input id="ugAvailableSearch" type="text" placeholder=" Search.." style="width: 100%; margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0;" /></td>
+<td></td>
+<td></td>
+</tr>
+<tr>
+<td>
+  <select id="ugAvailable" name="ugAvailable" multiple="multiple" style="height: 200px; width: 100%;"/>
+</td>
+<td>
+  <button type="button" style="width: 25px; text-align: center;" onclick="$.fn.selected.defaults.defaultMoveSelected('#ugAvailable');"><span class="fa fa-angle-right"> </span></button><br />
+  <button type="button" style="width: 25px; text-align: center;" onclick="$.fn.selected.defaults.defaultMoveSelected('#ugSelected');"><span class="fa fa-angle-left"> </span></button><br />
+  <button type="button" style="width: 25px; text-align: center;" onclick="$.fn.selected.defaults.defaultMoveAll('#ugAvailable');"><span class="fa fa-angle-double-right"> </span></button><br />
+  <button type="button" style="width: 25px; text-align: center;" onclick="$.fn.selected.defaults.defaultMoveAll('#ugSelected');"><span class="fa fa-angle-double-left"> </span></button><br />
+</td>
+<td>
+  <select id="ugSelected" name="ugSelected" multiple="multiple" style="height: 200px; width: 100%;">
+  #foreach ( $group in $user.groups )
+  <option value="$group.uid">$encoder.htmlEncode( $group.name )</option>
+  #end
+  </select>
+</td>
+</tr>
+</tbody>
+</table>
+
 <table>
     <colgroup>
       <col style="width: 500px;"/>