diff --git a/core/prepare_api.php b/core/prepare_api.php
index bf5c4581cb..741e7c72ba 100644
--- a/core/prepare_api.php
+++ b/core/prepare_api.php
@@ -61,11 +61,13 @@ function prepare_email_link( $p_email, $p_text ) {
}
/**
- * prepares the name of the user given the id. also makes it an email link.
- * @param integer $p_user_id A valid user identifier.
+ * Prepares the name of the user given the id.
+ * Also can make it a link to user info page.
+ * @param integer $p_user_id A valid user identifier.
+ * @param boolean $p_link Whether to include an html link
* @return string
*/
-function prepare_user_name( $p_user_id ) {
+function prepare_user_name( $p_user_id, $p_link = true ) {
# Catch a user_id of NO_USER (like when a handler hasn't been assigned)
if( NO_USER == $p_user_id ) {
return '';
@@ -82,7 +84,11 @@ function prepare_user_name( $p_user_id ) {
$t_name = string_display_line( $t_name );
if( user_exists( $p_user_id ) && user_get_field( $p_user_id, 'enabled' ) ) {
- return '' . $t_name . '';
+ if( $p_link ) {
+ return '' . $t_name . '';
+ } else {
+ return '' . $t_name . '';
+ }
}
return '' . $t_name . '';
diff --git a/core/project_api.php b/core/project_api.php
index f7bdc0e622..b6b90296ba 100644
--- a/core/project_api.php
+++ b/core/project_api.php
@@ -745,74 +745,121 @@ function project_get_upload_path( $p_project_id ) {
}
/**
- * add user with the specified access level to a project
+ * Add user with the specified access level to a project.
* @param integer $p_project_id A project identifier.
* @param integer $p_user_id A valid user id identifier.
* @param integer $p_access_level The access level to add the user with.
* @return void
*/
function project_add_user( $p_project_id, $p_user_id, $p_access_level ) {
- $t_access_level = (int)$p_access_level;
- if( DEFAULT_ACCESS_LEVEL == $t_access_level ) {
- # Default access level for this user
- $t_access_level = user_get_access_level( $p_user_id );
- }
-
- db_param_push();
- $t_query = 'INSERT INTO {project_user_list}
- ( project_id, user_id, access_level )
- VALUES
- ( ' . db_param() . ', ' . db_param() . ', ' . db_param() . ')';
-
- db_query( $t_query, array( (int)$p_project_id, (int)$p_user_id, $t_access_level ) );
+ project_add_users( $p_project_id, array( $p_user_id => $p_access_level ) );
}
/**
- * update entry
- * must make sure entry exists beforehand
+ * Update user with the specified access level to a project.
* @param integer $p_project_id A project identifier.
* @param integer $p_user_id A user identifier.
* @param integer $p_access_level Access level to set.
* @return void
*/
function project_update_user_access( $p_project_id, $p_user_id, $p_access_level ) {
- db_param_push();
- $t_query = 'UPDATE {project_user_list}
- SET access_level=' . db_param() . '
- WHERE project_id=' . db_param() . ' AND
- user_id=' . db_param();
-
- db_query( $t_query, array( (int)$p_access_level, (int)$p_project_id, (int)$p_user_id ) );
+ project_add_users( $p_project_id, array( $p_user_id => $p_access_level ) );
}
/**
- * update or add the entry as appropriate
- * This function involves one more database query than project_update_user_acces() or project_add_user()
+ * Update or add user with the specified access level to a project.
+ * This function involves one more database query than project_update_user_acces() or project_add_user().
* @param integer $p_project_id A project identifier.
* @param integer $p_user_id A user identifier.
* @param integer $p_access_level Project Access level to grant the user.
* @return boolean
*/
function project_set_user_access( $p_project_id, $p_user_id, $p_access_level ) {
- if( project_includes_user( $p_project_id, $p_user_id ) ) {
- return project_update_user_access( $p_project_id, $p_user_id, $p_access_level );
- } else {
- return project_add_user( $p_project_id, $p_user_id, $p_access_level );
+ project_add_users( $p_project_id, array( $p_user_id => $p_access_level ) );
+}
+
+/**
+ * Add or modify multiple users associated to a project with a specific access level.
+ * $p_changes is an array of access levels indexed by user_id, such as:
+ * array ( user1 => access_level, user2 => access_level, ... )
+ * This function will manage inserts and updates as needed.
+ *
+ * @param integer $p_project_id A project identifier.
+ * @param array $p_changes An array of modifications.
+ * @return void
+ */
+function project_add_users( $p_project_id, array $p_changes ) {
+ # normalize input
+ $t_changes = array();
+ foreach( $p_changes as $t_id => $t_value ) {
+ if( DEFAULT_ACCESS_LEVEL == $t_value ) {
+ $t_changes[(int)$t_id] = user_get_access_level( $t_id );
+ } else {
+ $t_changes[(int)$t_id] = (int)$t_value;
+ }
+ }
+
+ $t_user_ids = array_keys( $t_changes );
+ if( empty( $t_user_ids ) ) {
+ return;
+ }
+
+ $t_project_id = (int)$p_project_id;
+ $t_query = new DbQuery();
+ $t_sql = 'SELECT user_id FROM {project_user_list} WHERE project_id = ' . $t_query->param( $t_project_id )
+ . ' AND ' . $t_query->sql_in( 'user_id', $t_user_ids );
+ $t_query->sql( $t_sql );
+ $t_updating = array_column( $t_query->fetch_all(), 'user_id' );
+
+ if( !empty( $t_updating ) ) {
+ $t_update = new DbQuery( 'UPDATE {project_user_list} SET access_level = :new_value WHERE user_id = :user_id' );
+ foreach( $t_updating as $t_id ) {
+ $t_params = array( 'user_id' => (int)$t_id, 'new_value' => $t_changes[$t_id] );
+ $t_update->execute( $t_params );
+ unset( $t_changes[$t_id] );
+ }
+ }
+ # remaining items are for insert
+ if( !empty( $t_changes ) ) {
+ $t_insert = new DbQuery( 'INSERT INTO {project_user_list} ( project_id, user_id, access_level ) VALUES :params' );
+ foreach( $t_changes as $t_id => $t_value ) {
+ $t_insert->bind( 'params', array( $t_project_id, $t_id, $t_value ) );
+ $t_insert->execute();
+ }
}
}
/**
- * remove user from project
+ * Remove user from project.
* @param integer $p_project_id A project identifier.
* @param integer $p_user_id A user identifier.
* @return void
*/
function project_remove_user( $p_project_id, $p_user_id ) {
- db_param_push();
- $t_query = 'DELETE FROM {project_user_list}
- WHERE project_id=' . db_param() . ' AND user_id=' . db_param();
+ project_remove_users( $p_project_id, array( $p_user_id ) );
+}
+
+/**
+ * Remove multiple users from project.
+ * @param integer $p_project_id A project identifier.
+ * @param array $p_user_ids Array of user identifiers.
+ * @return type
+ */
+function project_remove_users( $p_project_id, array $p_user_ids ) {
+ # normalize input
+ $t_user_ids = array();
+ foreach( $p_user_ids as $t_id ) {
+ $t_user_ids[] = (int)$t_id;
+ }
+ if( empty( $t_user_ids ) ) {
+ return;
+ }
- db_query( $t_query, array( (int)$p_project_id, (int)$p_user_id ) );
+ $t_query = new DbQuery();
+ $t_sql = 'DELETE FROM {project_user_list} WHERE project_id = ' . $t_query->param( (int)$p_project_id )
+ . ' AND ' . $t_query->sql_in( 'user_id', $t_user_ids );
+ $t_query->sql( $t_sql );
+ $t_query->execute();
}
/**
diff --git a/css/ace-mantis.css b/css/ace-mantis.css
index 0e6c02e1a3..c74767f69a 100644
--- a/css/ace-mantis.css
+++ b/css/ace-mantis.css
@@ -274,14 +274,14 @@ pre {
.input-sm {
width: 90px !important;
- display: inline-block !important;
+ /*display: inline-block !important;*/ /* removed because it messes with js hide/show capabilities */
vertical-align: middle !important;
padding: 4px 6px !important;
}
.input-xs {
width: auto !important;
- display: inline-block !important;
+ /*display: inline-block !important;*/ /* removed because it messes with js hide/show capabilities */
vertical-align: middle !important;
padding: 1px 2px !important;
}
diff --git a/css/default.css b/css/default.css
index 88e8c30d22..e472557225 100644
--- a/css/default.css
+++ b/css/default.css
@@ -117,3 +117,44 @@ td.print-overdue { font-weight: bold; }
table.filters td.category {
color: #337ab7;
}
+
+.listjs-table .sort:after {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-bottom: 5px solid transparent;
+ content: "";
+ position: relative;
+ top: -10px;
+ right: -5px;
+}
+
+.listjs-table .sort.desc:after {
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 5px solid #707070;
+ content: "";
+ position: relative;
+ top: 4px;
+ right: -5px;
+}
+
+.listjs-table .sort.asc:after {
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-bottom: 5px solid #707070;
+ content: "";
+ position: relative;
+ top: -4px;
+ right: -5px;
+}
+
+.listjs-table .sort:hover {
+ text-decoration: underline;
+}
diff --git a/js/manage_proj_edit_page.js b/js/manage_proj_edit_page.js
new file mode 100644
index 0000000000..0a2aeaabda
--- /dev/null
+++ b/js/manage_proj_edit_page.js
@@ -0,0 +1,188 @@
+/*
+# Mantis - a php based bugtracking system
+
+# Copyright 2000 - 2002 Kenzaburo Ito - kenito@300baud.org
+# Copyright 2002 MantisBT Team - mantisbt-dev@lists.sourceforge.net
+
+# Mantis is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# Mantis is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mantis. If not, see .
+ */
+
+var userList;
+
+function acldelete_setup( div ) {
+ var jcheckboxs = $(div).find('input.user_access_delete');
+ var show_or_hide_select =
+ function(){
+ var jdivacl = $(div).closest('tr').find('div.editable_access_level');
+ if( this.checked ) {
+ jdivacl.hide()
+ } else {
+ jdivacl.show()
+ }
+ };
+ jcheckboxs.change(show_or_hide_select);
+ jcheckboxs.each(show_or_hide_select);
+}
+
+function acledit_setup( div ) {
+ var jdiv = $(div);
+ // add listeners to edit links
+ jdiv.find('span.unchanged a.edit_link').click( function(e){
+ e.preventDefault();
+ show_input( $(this).closest('div.editable_access_level') );
+ });
+ // add events to manage changes in selection
+ jdiv.find('select.user_access_level')
+ .on( 'blur change', function(e){
+ try_hide_input( $(this).closest('div.editable_access_level') );
+ var x1 = this.value;
+ var x2 = $(this).closest('.key-access');
+ var x3 = $(this).closest('.key-access').data('textvalue');
+ var selection = $(this).find('option:selected').text();
+ $(this).closest('.key-access').data('textvalue', selection);
+ var x4 = $(this).closest('.key-access').data('textvalue');
+ })
+ ;
+ try_hide_input( div );
+}
+
+function show_input( div ) {
+ var jdiv = $(div);
+ jdiv.find('span.changed_to').show();
+ jdiv.find('select.user_access_level').show().focus();
+ jdiv.find('span.unchanged').hide();
+}
+
+function try_hide_input( div ) {
+ var jdiv = $(div);
+ var jselect = jdiv.find('select.user_access_level');
+ if( jselect.is(":hidden") ) {
+ return;
+ }
+ var current_val = jselect.val();
+ var original_val = jselect.data('original_val');
+ if( current_val == original_val ) {
+ jselect.hide();
+ jdiv.find('span.changed_to').hide();
+ var textonly = jdiv.find('span.unchanged');
+ textonly.removeClass('hidden');
+ textonly.show();
+ }
+};
+
+
+$(document).ready( function() {
+
+ $('#manage-project-users-form-toolbox').removeClass('hidden');
+
+ var per_page = $('#input-per-page').val();
+ var userList_options = { valueNames: [ { name: 'key-name', attr: 'data-sortvalue' }, 'key-email', { name: 'key-access', attr: 'data-sortvalue' } ]
+ , page: per_page,
+ pagination: {
+ innerWindow: 2,
+ left: 1,
+ right: 1,
+ paginationClass: "pagination",
+ }
+ };
+ userList = new List('manage-project-users-list', userList_options);
+ userList.on( 'updated', function(){
+ $('div.editable_access_level').each( function(){ acledit_setup(this); } );
+ $('div.editable_user_delete').each( function(){ acldelete_setup(this); } );
+ });
+
+ $('#input-per-page').change( function(e) {
+ if( $.isNumeric( this.value ) && this.value > 0 ) {
+ userList.page = this.value;
+ userList.update();
+ }
+ });
+
+ $('div.editable_access_level').each( function(){ acledit_setup(this); } );
+ $('div.editable_user_delete').each( function(){ acldelete_setup(this); } );
+
+ $('#manage-project-users-form').submit( function(e){
+ var items = userList.items;
+
+ // Build an array of all inputs in list
+ // including those hidden under pagination
+ var acl_values = new Object();
+ var delete_ids = new Array();
+ $.each(items, function(){
+ var jselect = $(this.elm).find('select.user_access_level');
+ var current_val = jselect.val();
+ var original_val = jselect.data('original_val');
+ if( current_val != original_val ) {
+ var user_id = jselect.data('user_id');
+ acl_values[user_id] = current_val;
+ }
+ var jcheck = $(this.elm).find('input.user_access_delete:checkbox:checked');
+ if( jcheck.length ) {
+ delete_ids.push( jcheck.val() );
+ }
+ });
+
+ var json_submit = new Object();
+ json_submit['user_access_level'] = acl_values;
+ json_submit['user_access_delete'] = delete_ids;
+ console.log(JSON.stringify(json_submit));
+
+ $('').attr('type', 'hidden')
+ .attr('name', 'json_submit')
+ .attr('value', JSON.stringify(json_submit) )
+ .appendTo('#manage-project-users-form');
+ });
+
+ var btn_remove_all = $('#manage-project-users-form input[name=btn-remove-all]');
+ var btn_undo_remove_all = $('#manage-project-users-form button[name=btn-undo-remove-all]');
+ // remove hidden class and use jquery functionality.
+ btn_undo_remove_all.hide().removeClass('hidden');
+
+ btn_remove_all.click(function(e){
+ e.preventDefault();
+ var items = userList.items;
+ // Update al checkboxes in list
+ // including those hidden under pagination
+ $.each(items, function(){
+ var jcheck = $(this.elm).find('input.user_access_delete:checkbox');
+ if( jcheck.length ) {
+ // save previous state
+ jcheck.data('prev_state', jcheck.prop('checked') );
+ jcheck.prop('checked', true).trigger('change');
+ }
+ });
+ $(this).hide();
+ btn_undo_remove_all.show();
+ });
+
+ btn_undo_remove_all.click(function(e){
+ e.preventDefault();
+ var items = userList.items;
+ // Update al checkboxes in list
+ // including those hidden under pagination
+ $.each(items, function(){
+ var jcheck = $(this.elm).find('input.user_access_delete:checkbox');
+ if( jcheck.length ) {
+ // restore previous state
+ var prev = jcheck.data('prev_state');
+ if( undefined !== prev ) {
+ jcheck.prop('checked', prev).trigger('change');
+ }
+ }
+ });
+ $(this).hide();
+ btn_remove_all.show();
+ });
+
+});
diff --git a/lang/strings_english.txt b/lang/strings_english.txt
index ffb3f1daf7..c22c19ca8f 100644
--- a/lang/strings_english.txt
+++ b/lang/strings_english.txt
@@ -927,6 +927,9 @@ $s_create_new_subproject_link = 'Create New Subproject';
$s_unlink_link = 'Unlink';
$s_show_global_users = 'Show Users with Global Access';
$s_hide_global_users = 'Hide Users with Global Access';
+$s_review_changes = 'Review Changes';
+$s_review_changes_confirmation = 'Are you sure you want to apply the following changes?';
+$s_review_changes_empty = 'There are no changes selected';
# manage_proj_menu_page.php
$s_add_project_title = 'Add Project';
@@ -1103,6 +1106,8 @@ $s_add_user_button = 'Add User';
$s_project_selection_title = 'Project Selection';
$s_remove_link = 'Remove';
$s_remove_all_link = 'Remove all';
+$s_remove_project_user_title = 'Remove user access from project';
+$s_modify_project_user_title = 'Modify user access in project';
# proj_user_update.php
$s_updated_user_msg = 'Successfully updated user.';
@@ -1786,3 +1791,5 @@ $s_save = 'Save';
$s_reset = 'Reset';
$s_persist = 'Persist';
$s_load = 'Load';
+$s_apply_changes = 'Apply changes';
+$s_undo = 'Undo';
diff --git a/manage_proj_edit_page.php b/manage_proj_edit_page.php
index 7928bd0939..11b2f54a87 100644
--- a/manage_proj_edit_page.php
+++ b/manage_proj_edit_page.php
@@ -84,6 +84,8 @@
$t_can_manage_users = access_has_project_level( config_get( 'project_user_threshold' ), $f_project_id );
+require_js( 'manage_proj_edit_page.js' );
+
layout_page_header( project_get_field( $f_project_id, 'name' ) );
layout_page_begin( 'manage_overview_page.php' );
@@ -687,108 +689,177 @@
+
+
.
+
+/**
+ * @package MantisBT
+ * @copyright Copyright 2000 - 2002 Kenzaburo Ito - kenito@300baud.org
+ * @copyright Copyright 2002 MantisBT Team - mantisbt-dev@lists.sourceforge.net
+ * @link http://www.mantisbt.org
+ *
+ * @uses core.php
+ * @uses access_api.php
+ * @uses config_api.php
+ * @uses form_api.php
+ * @uses gpc_api.php
+ * @uses helper_api.php
+ * @uses html_api.php
+ * @uses lang_api.php
+ * @uses layout_api.php
+ * @uses print_api.php
+ * @uses project_api.php
+ * @uses user_api.php
+ */
+
+require_once( 'core.php' );
+require_api( 'access_api.php' );
+require_api( 'config_api.php' );
+require_api( 'form_api.php' );
+require_api( 'gpc_api.php' );
+require_api( 'helper_api.php' );
+require_api( 'html_api.php' );
+require_api( 'lang_api.php' );
+require_api( 'layout_api.php' );
+require_api( 'print_api.php' );
+require_api( 'project_api.php' );
+require_api( 'user_api.php' );
+
+form_security_validate( 'manage_proj_user_update' );
+auth_reauthenticate();
+
+$f_project_id = gpc_get_int( 'project_id' );
+$f_confirmed = gpc_get_bool( '_confirmed' );
+
+project_ensure_exists( $f_project_id );
+access_ensure_project_level( config_get( 'manage_project_threshold' ), $f_project_id );
+access_ensure_project_level( config_get( 'project_user_threshold' ), $f_project_id );
+
+# If the form was managed by javascript, we recive relevant data in a json array
+# so we can ignore the standard inputs
+$f_form_json_updates = gpc_get_string( 'json_submit', null );
+if( $f_form_json_updates ) {
+ $t_form_updates = json_decode( $f_form_json_updates, true );
+ $f_form_updated_acls = $t_form_updates['user_access_level'];
+ $f_form_deleted_ids = $t_form_updates['user_access_delete'];
+} else {
+ $f_form_updated_acls = gpc_get_int_array( 'user_access_level', array() );
+ $f_form_deleted_ids = gpc_get_int_array( 'user_access_delete', array() );
+}
+
+# Build and validate the set of changes
+
+$t_users_to_update = array();
+$t_users_to_delete = array();
+
+$t_all_local_users = project_get_all_user_rows( $f_project_id, ANYBODY, false );
+
+foreach( $f_form_deleted_ids as $t_id ) {
+ if( !isset( $t_all_local_users[$t_id] ) ) {
+ # a user id that is not explicitly assigned to the project
+ continue;
+ }
+ $t_user = $t_all_local_users[$t_id];
+ $t_can_manage_this_user = access_has_project_level( $t_user['access_level'], $f_project_id );
+ if( !$t_can_manage_this_user ) {
+ # this user can't be modified
+ continue;
+ }
+ $t_users_to_delete[$t_id] = $t_id;
+}
+
+$t_current_user_access_level = access_get_project_level( $f_project_id );
+$t_enum_values = MantisEnum::getValues( config_get( 'access_levels_enum_string' ) );
+
+foreach( $f_form_updated_acls as $t_id => $t_value ) {
+ if( isset( $t_users_to_delete[$t_id] ) ) {
+ # this user is marked for deletion, ignore it
+ continue;
+ }
+ if( !isset( $t_all_local_users[$t_id] ) ) {
+ # a user id that is not explicitly assigned to the project
+ continue;
+ }
+ $t_user = $t_all_local_users[$t_id];
+ if( $t_value == $t_user['access_level'] ) {
+ # the value is not changed, ignore it
+ continue;
+ }
+ $t_can_manage_this_user = access_has_project_level( $t_user['access_level'], $f_project_id );
+ if( !$t_can_manage_this_user ) {
+ # this user can't be modified
+ continue;
+ }
+
+ if( $t_value > $t_current_user_access_level ) {
+ # can't assign a higer level that the one current use has
+ continue;
+ }
+ if( !in_array( $t_value, $t_enum_values ) ) {
+ # the submitted access level is not valid
+ continue;
+ }
+
+ $t_users_to_update[$t_id] = $t_value;
+}
+
+if( !$f_confirmed ) {
+ # Display a confirmation page with a summary of changes
+
+ $t_affected_users = array_merge( array_keys( $t_users_to_update ), array_keys( $t_users_to_delete ) );
+
+ layout_page_header();
+ layout_page_begin();
+ ?>
+
+
+
+
+
+
+
+
+ $t_val ) {
+ $t_usernames_val[] = user_get_name( $t_id );
+ }
+ $t_update_key = array_keys( $t_users_to_update );
+ $t_update_val = array_values( $t_users_to_update );
+ array_multisort( $t_usernames_val, SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $t_update_key, $t_update_val );
+ $t_users_to_update = array_combine( $t_update_key, $t_update_val );
+ ?>
+
+
+
+
+
+