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 @@
-
-
-
-
-

- - -

-
-
-
-
- - - - - - - -
-
-
-
-
-
- - - - - - - - - - - +
+
+
+

+ + +

+
+
- for( $i = 0; $i < $t_users_count; $i++ ) { - $t_user = $t_users[$i]; -?> -
- - + + + +
- - - - +
+
+
+ + + + + + + +
+
+
+ 0 ) { + + $t_user_ids = array(); + $t_sort = array(); + foreach ( $t_users as $t_ix => $t_user ) { + $t_user_display_name = user_get_name_from_row( $t_user ); + $t_users[$t_ix]['display_name'] = $t_user_display_name; + $t_user_ids[] = $t_user['id']; + $t_sort[] = $t_user_display_name; + } + + user_cache_array_rows( $t_user_ids ); + array_multisort( $t_sort, SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $t_users ); + + ?> + + +
+
+ + +
+ + + + + + + + + + + + + + + + - - - - - -
+ + + + + + '; + echo $t_current_level_string . ''; + echo ' => '; + echo ''; + echo ''; + echo ''; + } else { + echo $t_current_level_string; + } ?> - $f_project_id, 'user_id' => $t_user['id'] ), - $t_token_remove_user ); - $t_removable_users_exist = true; - } - } ?> -
-
-
- -
- +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ + + +
+
+ +
+
+ 0 + ?> +
+ $f_project_id ), - $t_token_remove_user ); - } ?> -
-
-
+
+ + . + +/** + * @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(); + ?> +
+
+ +
+
+

+ + +

+
+
+
+ +
+

+
+
+ +
+

+
+ +
+ + + +
+ +
+
+ +
+
+
+ +
+ +
+
+

+ + +

+
+
+
+
+ + + + + + + + + '; + }; + foreach( $t_users_to_delete as $t_id ) { + $t_username = prepare_user_name( $t_id, false ); + $t_str_acl = get_enum_element( 'access_levels', $t_all_local_users[$t_id]['access_level'] ); + $fn_print_tr( $t_username, $t_str_acl ); + } + ?> + +
' . $p_td1 . '' . $p_td2 . '
+
+
+
+
+ $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 ); + ?> +
+ +
+
+

+ + +

+
+
+
+
+ + + + + + + + + + '; + }; + foreach( $t_users_to_update as $t_id => $t_new_acl ) { + $t_username = prepare_user_name( $t_id, false ); + $t_old_acl = $t_all_local_users[$t_id]['access_level']; + $t_str_new_acl = get_enum_element( 'access_levels', $t_new_acl ); + $t_str_old_acl = get_enum_element( 'access_levels', $t_old_acl ); + $fn_print_tr( $t_username, $t_str_old_acl, $t_str_new_acl ); + } + ?> + +
' . $p_td1 . '' . $p_td2 . '' . $p_td3 . '
+
+
+
+
+ +
+