Merge "Refactoring of user assignment workflow."

This commit is contained in:
Jenkins 2013-08-10 00:43:24 +00:00 committed by Gerrit Code Review
commit 79f503d477
28 changed files with 650 additions and 988 deletions

View File

@ -0,0 +1,484 @@
/* Namespace for core functionality related to Membership Workflow Step. */
horizon.membership = {
current_membership: [],
data: [],
roles: [],
has_roles: [],
default_role_id: [],
/* Parses the form field selector's ID to get either the
* role or user id (i.e. returns "id12345" when
* passed the selector with id: "id_group_id12345").
**/
get_field_id: function(id_string) {
return id_string.slice(id_string.lastIndexOf("_") + 1);
},
/*
* Gets the html select element associated with a given
* role id.
**/
get_role_element: function(step_slug, role_id) {
return $('select[id^="id_' + step_slug + '_role_' + role_id + '"]');
},
/*
* Gets the html ul element associated with a given
* data id. I.e., the member's row.
**/
get_member_element: function(step_slug, data_id) {
return $('li[data-' + step_slug + '-id$=' + data_id + ']').parent();
},
/*
* Initializes all of the horizon.membership lists with
* data parsed from the hidden form fields, as well as the
* default role id.
**/
init_properties: function(step_slug) {
horizon.membership.has_roles[step_slug] = $("." + step_slug + "_membership").data('show-roles') !== "no";
horizon.membership.default_role_id[step_slug] = $('#id_default_' + step_slug + '_role').attr('value');
horizon.membership.init_data_list(step_slug);
horizon.membership.init_role_list(step_slug);
horizon.membership.init_current_membership(step_slug);
},
/*
* Initializes an associative array mapping data ids to display names.
**/
init_data_list: function(step_slug) {
horizon.membership.data[step_slug] = [];
_.each($(this.get_role_element(step_slug, "")).find("option"), function (option) {
horizon.membership.data[step_slug][option.value] = option.text;
});
},
/*
* Initializes an associative array mapping role ids to role names.
**/
init_role_list: function(step_slug) {
horizon.membership.roles[step_slug] = [];
_.each($('label[for^="id_' + step_slug + '_role_"]'), function(role) {
var id = horizon.membership.get_field_id($(role).attr('for'));
horizon.membership.roles[step_slug][id] = $(role).text();
});
},
/*
* Initializes an associative array of lists of the current
* members for each available role.
**/
init_current_membership: function(step_slug) {
horizon.membership.current_membership[step_slug] = [];
var members_list = [];
var role_name, role_id, selected_members;
_.each(this.get_role_element(step_slug, ''), function(value, key) {
role_id = horizon.membership.get_field_id($(value).attr('id'));
role_name = $('label[for="id_' + step_slug + '_role_' + role_id + '"]').text();
// get the array of members who are selected in this list
selected_members = $(value).find("option:selected");
// extract the member names and add them to the dictionary of lists
members_list = [];
if (selected_members) {
_.each(selected_members, function(member) {
members_list.push(member.value);
});
}
horizon.membership.current_membership[step_slug][role_id] = members_list;
});
},
/*
* Returns the ids of roles the data is member of.
**/
get_member_roles: function(step_slug, data_id) {
var roles = [];
for (var role in horizon.membership.current_membership[step_slug]) {
if ($.inArray(data_id, horizon.membership.current_membership[step_slug][role]) >= 0) {
roles.push(role);
}
}
return roles;
},
/*
* Updates the selected values on the role_list's form field, as
* well as the current_membership dictionary's list.
**/
update_role_lists: function(step_slug, role_id, new_list) {
this.get_role_element(step_slug, role_id).val(new_list);
horizon.membership.current_membership[step_slug][role_id] = new_list;
},
/*
* Helper function for remove_member_from_role.
**/
remove_member: function(step_slug, data_id, role_id, role_list) {
var index = role_list.indexOf(data_id);
if (index >= 0) {
// remove member from list
role_list.splice(index, 1);
horizon.membership.update_role_lists(step_slug, role_id, role_list);
}
},
/*
* Searches through the role lists and removes a given member
* from the lists.
**/
remove_member_from_role: function(step_slug, data_id, role_id) {
var role_list;
if (role_id) {
role_list = horizon.membership.current_membership[step_slug][role_id];
horizon.membership.remove_member(step_slug, data_id, role_id, role_list);
}
else {
// search for membership in role lists
for (var role in horizon.membership.current_membership[step_slug]) {
role_list = horizon.membership.current_membership[step_slug][role];
horizon.membership.remove_member(step_slug, data_id, role, role_list);
}
}
},
/*
* Adds a member to a given role list.
**/
add_member_to_role: function(step_slug, data_id, role_id) {
var role_list = horizon.membership.current_membership[step_slug][role_id];
role_list.push(data_id);
horizon.membership.update_role_lists(step_slug, role_id, role_list);
},
update_member_role_dropdown: function(step_slug, data_id, role_ids, member_el) {
if (typeof(role_ids) === 'undefined') {
role_ids = horizon.membership.get_member_roles(step_slug, data_id);
}
if (typeof(member_el) === 'undefined') {
member_el = horizon.membership.get_member_element(step_slug, data_id);
}
var $dropdown = member_el.find("li.member").siblings('.dropdown');
var $role_items = $dropdown.children('.role_dropdown').children('li');
$role_items.each(function (idx, el) {
if (_.contains(role_ids, $(el).data('role-id'))) {
$(el).addClass('selected');
} else {
$(el).removeClass('selected');
}
});
// set the selection back to default role
var $roles_display = $dropdown.children('.dropdown-toggle').children('.roles_display');
var roles_to_display = [];
for (var i = 0; i < role_ids.length; i++) {
if (i == 2) {
roles_to_display.push('...');
break;
}
roles_to_display.push(horizon.membership.roles[step_slug][role_ids[i]]);
}
text = roles_to_display.join(', ');
if (text.length == 0) text = gettext('No roles');
$roles_display.text(text);
},
/*
* Generates the HTML structure for a member that will be displayed
* as a list item in the member list.
**/
generate_member_element: function(step_slug, display_name, data_id, role_ids, text) {
var str_id = "id_" + step_slug + "_" + data_id;
var roles = [];
for (var r in horizon.membership.roles[step_slug]) {
var role = {};
role['role_id'] = r;
role['role_name'] = horizon.membership.roles[step_slug][r];
roles.push(role);
}
var template = horizon.templates.compiled_templates["#membership_template"],
params = {data_id: str_id,
step_slug: step_slug,
default_role: horizon.membership.roles[horizon.membership.default_role_id[step_slug]],
display_name: display_name,
text: text,
roles: roles},
member_el = $(template.render(params));
this.update_member_role_dropdown(step_slug, str_id, role_ids, member_el);
return $(member_el);
},
/*
* Generates the HTML structure for the membership UI.
**/
generate_html: function(step_slug) {
var data;
for (data in horizon.membership.data[step_slug]) {
var data_id = data;
var display_name = horizon.membership.data[step_slug][data_id];
var role_ids = this.get_member_roles(step_slug, data_id);
if (role_ids.length > 0) {
$("." + step_slug + "_members").append(this.generate_member_element(step_slug, display_name, data_id, role_ids, "-"));
}
else {
$(".available_" + step_slug).append(this.generate_member_element(step_slug, display_name, data_id, role_ids, "+"));
}
}
horizon.membership.detect_no_results(step_slug);
},
/*
* Triggers on click of link to add/remove membership association.
**/
update_membership: function(step_slug) {
$(".available_" + step_slug + ", ." + step_slug + "_members").on('click', ".btn-group a[href='#add_remove']", function (evt) {
evt.preventDefault();
var available = $(".available_" + step_slug).has($(this)).length;
var data_id = horizon.membership.get_field_id($(this).parent().siblings().attr('data-' + step_slug + '-id'));
var member_el = $(this).parent().parent();
if (available) {
var default_role = horizon.membership.default_role_id[step_slug];
$(this).text("-");
$("." + step_slug + "_members").append(member_el);
horizon.membership.add_member_to_role(step_slug, data_id, default_role);
if (horizon.membership.has_roles[step_slug]) {
$(this).parent().siblings(".role_options").show();
horizon.membership.update_member_role_dropdown(step_slug, data_id, [default_role], member_el);
}
}
else {
$(this).text("+");
$(this).parent().siblings(".role_options").hide();
$(".available_" + step_slug).append(member_el);
horizon.membership.remove_member_from_role(step_slug, data_id);
}
// update lists
horizon.membership.list_filtering(step_slug);
horizon.membership.detect_no_results(step_slug);
// remove input filters
$("input." + step_slug + "_filter").val("");
});
},
/*
* Detects whether each list has members and if it does not
* displays a message to the user.
**/
detect_no_results: function (step_slug) {
$('.' + step_slug + '_filterable').each( function () {
var css_class = $(this).find('ul').attr('class');
// Example value: members step_slug_members
// Pick the class name that contains the step_slug
var filter = _.find(css_class.split(' '), function(val){ return val.indexOf(step_slug) != -1; });
if (!$('.' + filter).children('ul').length) {
$('#no_' + filter).show();
$("input[id='" + filter + "']").attr('disabled', 'disabled');
}
else {
$('#no_' + filter).hide();
$("input[id='" + filter + "']").removeAttr('disabled');
}
});
},
/*
* Triggers on selection of new role for a member.
**/
select_member_role: function(step_slug) {
$(".available_" + step_slug + ", ." + step_slug + "_members").on('click', '.role_dropdown li', function (evt) {
evt.preventDefault();
evt.stopPropagation();
// get the newly selected role and the member's name
var new_role_id = $(this).attr("data-role-id");
var id_str = $(this).parent().parent().siblings(".member").attr("data-" + step_slug + "-id");
var data_id = horizon.membership.get_field_id(id_str);
// update role lists
if ($(this).hasClass('selected')) {
$(this).removeClass('selected');
horizon.membership.remove_member_from_role(step_slug, data_id, new_role_id);
} else {
$(this).addClass('selected');
horizon.membership.add_member_to_role(step_slug, data_id, new_role_id);
}
horizon.membership.update_member_role_dropdown(step_slug, data_id);
});
},
/*
* Triggers on the addition of a new member via the inline object creation field.
**/
add_new_member: function(step_slug) {
$("select[id='id_new_" + step_slug + "']").on('change', function (evt) {
// add the member to the visible list
var display_name = $(this).find("option").text();
var data_id = $(this).find("option").attr("value");
var default_role_id = horizon.membership.default_role_id[step_slug];
$("." + step_slug + "_members").append(horizon.membership.generate_member_element(step_slug, display_name, data_id, [default_role_id], "-"));
// add the member to the hidden role lists and the data list
horizon.membership.data[step_slug][data_id] = display_name;
$("select[multiple='multiple']").append("<option value='" + data_id + "'>" + horizon.membership.data[step_slug][data_id] + "</option>");
horizon.membership.add_member_to_role(step_slug, data_id, default_role_id);
// remove option from hidden select
$(this).text("");
// reset lists and input filters
horizon.membership.list_filtering(step_slug);
horizon.membership.detect_no_results(step_slug);
$("input.filter").val("");
// fix styling
$("." + step_slug + "_members .btn-group").removeClass('last_stripe');
$("." + step_slug + "_members .btn-group:last").addClass('last_stripe');
});
},
/*
* Style the inline object creation button, hide the associated field.
**/
add_new_member_styling: function(step_slug) {
var add_member_el = $("label[for='id_new_" + step_slug + "']").parent();
$(add_member_el).find("select").hide();
$("#add_" + step_slug).append($(add_member_el));
$(add_member_el).addClass("add_" + step_slug);
$(add_member_el).find("label, .input").addClass("add_" + step_slug + "_btn");
},
/*
* Fixes the striping of the fake table upon modification of the lists.
**/
fix_stripes: function(step_slug) {
$('.fake_' + step_slug + '_table').each( function () {
var filter = "." + $(this).attr('id');
var visible = " .btn-group:visible";
var even = " .btn-group:visible:even";
var last = " .btn-group:visible:last";
// fix striping of rows
$(filter + visible).removeClass('dark_stripe');
$(filter + visible).addClass('light_stripe');
$(filter + even).removeClass('light_stripe');
$(filter + even).addClass('dark_stripe');
// fix bottom border of new last element
$(filter + visible).removeClass('last_stripe');
$(filter + last).addClass('last_stripe');
});
},
/*
* Sets up filtering for each list of data.
**/
list_filtering: function (step_slug) {
// remove previous lists' quicksearch events
$('input.' + step_slug + '_filter').unbind();
// set up quicksearch to filter on input
$('.' + step_slug + '_filterable').each(function () {
var css_class = $(this).children().children('ul').attr('class');
// Example value: members step_slug_members
// Pick the class name that contains the step_slug
var filter = _.find(css_class.split(' '), function(val){ return val.indexOf(step_slug) != -1; });
var input = $("input[id='" + filter +"']");
input.quicksearch('ul.' + filter + ' ul li span.display_name', {
'delay': 200,
'loader': 'span.loading',
'show': function () {
$(this).parent().parent().show();
if (filter == "available_" + step_slug) {
$(this).parent('.dropdown-toggle').hide();
}
},
'hide': function () {
$(this).parent().parent().hide();
},
'noResults': 'ul#no_' + filter,
'onAfter': function () {
horizon.membership.fix_stripes(step_slug);
},
'prepareQuery': function (val) {
return new RegExp(val, "i");
},
'testQuery': function (query, txt, span) {
if ($(input).attr('id') == filter) {
$(input).prev().removeAttr('disabled');
return query.test($(span).text());
}
else
return true;
}
});
});
},
/*
* Calls set-up functions upon loading the workflow.
**/
workflow_init: function(modal, step_slug, step_id) {
// fix the dropdown menu overflow issues
$(".tab-content, .workflow").addClass("dropdown_fix");
$(modal).find('form').each( function () {
var $form = $(this);
// Do nothing if this isn't a membership modal
if ($form.find('div.' + step_slug + '_membership').length == 0) {
return; // continue
}
// call the initalization functions
horizon.membership.init_properties(step_slug);
horizon.membership.generate_html(step_slug);
horizon.membership.update_membership(step_slug);
horizon.membership.select_member_role(step_slug);
horizon.membership.add_new_member(step_slug);
// initially hide role dropdowns for available member list
$form.find(".available_" + step_slug + " .role_options").hide();
// hide the dropdown for members too if we don't need to show it
if (!horizon.membership.has_roles[step_slug]) {
$form.find("." + step_slug + "_members .role_options").hide();
}
// unfocus filter fields
if (step_id.indexOf('update') ==0) {
$form.find("#" + step_id + " input").blur();
}
// prevent filter inputs from submitting form on 'enter'
$form.find('.' + step_slug + '_membership').keydown(function(event){
if(event.keyCode == 13) {
event.preventDefault();
return false;
}
});
// add filtering + styling to the inline obj creation btn
horizon.membership.add_new_member_styling(step_slug);
horizon.membership.list_filtering(step_slug);
horizon.membership.detect_no_results(step_slug);
// fix initial striping of rows
$form.find('.fake_' + step_slug + '_table').each( function () {
var filter = "." + $(this).attr('id');
$(filter + ' .btn-group:even').addClass('dark_stripe');
$(filter + ' .btn-group:last').addClass('last_stripe');
});
});
}
};

View File

@ -1,474 +0,0 @@
/* Namespace for core functionality related to Project Workflows. */
horizon.projects = {
current_membership: [],
users: [],
roles: [],
has_roles: true,
default_role_id: "",
/* Parses the form field selector's ID to get either the
* role or user id (i.e. returns "id12345" when
* passed the selector with id: "id_user_id12345").
**/
get_field_id: function(id_string) {
return id_string.slice(id_string.lastIndexOf("_") + 1);
},
/*
* Gets the html select element associated with a given
* role id.
**/
get_role_element: function(role_id) {
return $('select[id^="id_role_' + role_id + '"]');
},
/*
* Gets the html ul element associated with a given
* user id. I.e., the user's row.
**/
get_user_element: function(user_id) {
return $('li[data-user-id$=' + user_id + ']').parent();
},
/*
* Initializes all of the horizon.projects lists with
* data parsed from the hidden form fields, as well as the
* default role id.
**/
init_properties: function() {
horizon.projects.has_roles = $(".project_membership").data('show-roles') !== "no";
horizon.projects.default_role_id = $('#id_default_role').attr('value');
horizon.projects.init_user_list();
horizon.projects.init_role_list();
horizon.projects.init_current_membership();
},
/*
* Initializes an associative array mapping user ids to user names.
**/
init_user_list: function() {
horizon.projects.users = [];
_.each($(this.get_role_element("")).find("option"), function (option) {
horizon.projects.users[option.value] = option.text;
});
},
/*
* Initializes an associative array mapping role ids to role names.
**/
init_role_list: function() {
horizon.projects.roles = [];
_.each($('label[for^="id_role_"]'), function(role) {
var id = horizon.projects.get_field_id($(role).attr('for'));
horizon.projects.roles[id] = $(role).text();
});
},
/*
* Initializes an associative array of lists of the current
* members for each available role.
**/
init_current_membership: function() {
horizon.projects.current_membership = [];
var members_list = [];
var role_name, role_id, selected_members;
_.each(this.get_role_element(''), function(value, key) {
role_id = horizon.projects.get_field_id($(value).attr('id'));
role_name = $('label[for="id_role_' + role_id + '"]').text();
// get the array of members who are selected in this list
selected_members = $(value).find("option:selected");
// extract the member names and add them to the dictionary of lists
members_list = [];
if (selected_members) {
_.each(selected_members, function(member) {
members_list.push(member.value);
});
}
horizon.projects.current_membership[role_id] = members_list;
});
},
/*
* Returns the ids of roles the user is a member of.
**/
get_user_roles: function(user_id) {
var roles = [];
for (var role in horizon.projects.current_membership) {
if ($.inArray(user_id, horizon.projects.current_membership[role]) >= 0) {
roles.push(role);
}
}
return roles;
},
/*
* Updates the selected values on the role_list's form field, as
* well as the current_membership dictionary's list.
**/
update_role_lists: function(role_id, new_list) {
this.get_role_element(role_id).val(new_list);
horizon.projects.current_membership[role_id] = new_list;
},
/*
* Helper function for remove_user_from_role.
**/
remove_user: function(user_id, role_id, role_list) {
var index = role_list.indexOf(user_id);
if (index >= 0) {
// remove member from list
role_list.splice(index, 1);
horizon.projects.update_role_lists(role_id, role_list);
}
},
/*
* Searches through the role lists and removes a given user
* from the lists.
**/
remove_user_from_role: function(user_id, role_id) {
var role_list;
if (role_id) {
role_list = horizon.projects.current_membership[role_id];
horizon.projects.remove_user(user_id, role_id, role_list);
}
else {
// search for membership in role lists
for (var role in horizon.projects.current_membership) {
role_list = horizon.projects.current_membership[role];
horizon.projects.remove_user(user_id, role, role_list);
}
}
},
/*
* Adds a given user to a given role list.
**/
add_user_to_role: function(user_id, role_id) {
var role_list = horizon.projects.current_membership[role_id];
role_list.push(user_id);
horizon.projects.update_role_lists(role_id, role_list);
},
update_user_role_dropdown: function(user_id, role_ids, user_el) {
if (typeof(role_ids) === 'undefined') {
role_ids = horizon.projects.get_user_roles(user_id);
}
if (typeof(user_el) === 'undefined') {
user_el = horizon.projects.get_user_element(user_id);
}
var $dropdown = user_el.find("li.member").siblings('.dropdown');
var $role_items = $dropdown.children('.role_dropdown').children('li');
$role_items.each(function (idx, el) {
if (_.contains(role_ids, $(el).data('role-id'))) {
$(el).addClass('selected');
} else {
$(el).removeClass('selected');
}
});
// set the selection back to default role
var $roles_display = $dropdown.children('.dropdown-toggle').children('.roles_display');
var roles_to_display = [];
for (var i = 0; i < role_ids.length; i++) {
if (i == 2) {
roles_to_display.push('...');
break;
}
roles_to_display.push(horizon.projects.roles[role_ids[i]]);
}
text = roles_to_display.join(', ');
if (text.length == 0) text = 'No roles';
$roles_display.text(text);
},
/*
* Generates the HTML structure for a user that will be displayed
* as a list item in the project member list.
**/
generate_user_element: function(user_name, user_id, role_ids, text) {
var str_id = "id_user_" + user_id;
var roles = [];
for (var r in horizon.projects.roles) {
var role = {};
role['role_id'] = r;
role['role_name'] = horizon.projects.roles[r];
roles.push(role);
}
var template = horizon.templates.compiled_templates["#project_user_template"],
params = {user_id: str_id,
default_role: horizon.projects.roles[horizon.projects.default_role_id],
user_name: user_name,
text: text,
roles: roles},
user_el = $(template.render(params));
this.update_user_role_dropdown(str_id, role_ids, user_el);
return $(user_el);
},
/*
* Generates the HTML structure for the project membership UI.
**/
generate_html: function() {
var user;
for (user in horizon.projects.users) {
var user_id = user;
var user_name = horizon.projects.users[user];
var role_ids = this.get_user_roles(user_id);
if (role_ids.length > 0) {
$(".project_members").append(this.generate_user_element(user_name, user_id, role_ids, "-"));
}
else {
$(".available_users").append(this.generate_user_element(user_name, user_id, role_ids, "+"));
}
}
horizon.projects.detect_no_results();
},
/*
* Triggers on click of link to add/remove member from the project.
**/
update_membership: function() {
$(".available_users, .project_members").on('click', ".btn-group a[href='#add_remove']", function (evt) {
evt.preventDefault();
var available = $(".available_users").has($(this)).length;
var user_id = horizon.projects.get_field_id($(this).parent().siblings().attr('data-user-id'));
var user_el = $(this).parent().parent();
if (available) {
var default_role = horizon.projects.default_role_id;
$(this).text("-");
$(".project_members").append(user_el);
horizon.projects.add_user_to_role(user_id, default_role);
if (horizon.projects.has_roles) {
$(this).parent().siblings(".role_options").show();
horizon.projects.update_user_role_dropdown(user_id, [default_role], user_el);
}
}
else {
$(this).text("+");
$(this).parent().siblings(".role_options").hide();
$(".available_users").append(user_el);
horizon.projects.remove_user_from_role(user_id);
}
// update lists
horizon.projects.list_filtering();
horizon.projects.detect_no_results();
// remove input filters
$("input.filter").val("");
});
},
/*
* Detects whether each list has members and if it does not
* displays a message to the user.
**/
detect_no_results: function () {
$('.filterable').each( function () {
var filter = $(this).find('ul').attr('class');
if (!$('.' + filter).children('ul').length) {
$('#no_' + filter).show();
$("input[id='" + filter + "']").attr('disabled', 'disabled');
}
else {
$('#no_' + filter).hide();
$("input[id='" + filter + "']").removeAttr('disabled');
}
});
},
/*
* Triggers on selection of new role for a member.
**/
select_member_role: function() {
$(".available_users, .project_members").on('click', '.role_dropdown li', function (evt) {
evt.preventDefault();
evt.stopPropagation();
// get the newly selected role and the member's name
var new_role_id = $(this).attr("data-role-id");
var id_str = $(this).parent().parent().siblings(".member").attr("data-user-id");
var user_id = horizon.projects.get_field_id(id_str);
// update role lists
if ($(this).hasClass('selected')) {
$(this).removeClass('selected');
horizon.projects.remove_user_from_role(user_id, new_role_id);
} else {
$(this).addClass('selected');
horizon.projects.add_user_to_role(user_id, new_role_id);
}
horizon.projects.update_user_role_dropdown(user_id);
});
},
/*
* Triggers on the addition of a new user via the inline object creation field.
**/
add_new_user: function() {
$("select[id='id_new_user']").on('change', function (evt) {
// add the user to the visible list
var user_name = $(this).find("option").text();
var user_id = $(this).find("option").attr("value");
var default_role_id = horizon.projects.default_role_id;
$(".project_members").append(horizon.projects.generate_user_element(user_name, user_id, [default_role_id], "-"));
// add the user to the hidden role lists and the users list
horizon.projects.users[user_id] = user_name;
$("select[multiple='multiple']").append("<option value='" + user_id + "'>" + horizon.projects.users[user_id] + "</option>");
horizon.projects.add_user_to_role(user_id, default_role_id);
// remove option from hidden select
$(this).text("");
// reset lists and input filters
horizon.projects.list_filtering();
horizon.projects.detect_no_results();
$("input.filter").val("");
// fix styling
$(".project_members .btn-group").removeClass('last_stripe');
$(".project_members .btn-group:last").addClass('last_stripe');
});
},
/*
* Style the inline object creation button, hide the associated field.
**/
add_new_user_styling: function() {
var add_user_el = $("label[for='id_new_user']").parent();
$(add_user_el).find("select").hide();
$("#add_user").append($(add_user_el));
$(add_user_el).addClass("add_user");
$(add_user_el).find("label, .input").addClass("add_user_btn");
},
/*
* Fixes the striping of the fake table upon modification of the lists.
**/
fix_stripes: function() {
$('.fake_table').each( function () {
var filter = "." + $(this).attr('id');
var visible = " .btn-group:visible";
var even = " .btn-group:visible:even";
var last = " .btn-group:visible:last";
// fix striping of rows
$(filter + visible).removeClass('dark_stripe');
$(filter + visible).addClass('light_stripe');
$(filter + even).removeClass('light_stripe');
$(filter + even).addClass('dark_stripe');
// fix bottom border of new last element
$(filter + visible).removeClass('last_stripe');
$(filter + last).addClass('last_stripe');
});
},
/*
* Sets up filtering for each list of users.
**/
list_filtering: function () {
// remove previous lists' quicksearch events
$('input.filter').unbind();
// set up quicksearch to filter on input
$('.filterable').each(function () {
var filter = $(this).children().children('ul').attr('class');
var input = $("input[id='" + filter +"']");
input.quicksearch('ul.' + filter + ' ul li span.user_name', {
'delay': 200,
'loader': 'span.loading',
'show': function () {
$(this).parent().parent().show();
if (filter == "available_users") {
$(this).parent('.dropdown-toggle').hide();
}
},
'hide': function () {
$(this).parent().parent().hide();
},
'noResults': 'ul#no_' + filter,
'onAfter': function () {
horizon.projects.fix_stripes();
},
'prepareQuery': function (val) {
return new RegExp(val, "i");
},
'testQuery': function (query, txt, span) {
if ($(input).attr('id') == filter) {
$(input).prev().removeAttr('disabled');
return query.test($(span).text());
}
else
return true;
}
});
});
},
/*
* Calls set-up functions upon loading the workflow.
**/
workflow_init: function(modal) {
// fix the dropdown menu overflow issues
$(".tab-content, .workflow").addClass("dropdown_fix");
$(modal).find('form').each( function () {
var $form = $(this);
// Do nothing if this isn't a membership modal
if ($form.find('div.project_membership').length == 0) {
return; // continue
}
// call the initalization functions
horizon.projects.init_properties();
horizon.projects.generate_html();
horizon.projects.update_membership();
horizon.projects.select_member_role();
horizon.projects.add_new_user();
// initially hide role dropdowns for available users list
$form.find(".available_users .role_options").hide();
// hide the dropdown for members too if we don't need to show it
if (!horizon.projects.has_roles) {
$form.find(".project_members .role_options").hide();
}
// unfocus filter fields
$form.find("#update_project__update_members input").blur();
// prevent filter inputs from submitting form on 'enter'
$form.find('.project_membership').keydown(function(event){
if(event.keyCode == 13) {
event.preventDefault();
return false;
}
});
// add filtering + styling to the inline obj creation btn
horizon.projects.add_new_user_styling();
horizon.projects.list_filtering();
horizon.projects.detect_no_results();
// fix initial striping of rows
$form.find('.fake_table').each( function () {
var filter = "." + $(this).attr('id');
$(filter + ' .btn-group:even').addClass('dark_stripe');
$(filter + ' .btn-group:last').addClass('last_stripe');
});
});
}
};

View File

@ -1,6 +1,6 @@
/* Namespace for core functionality related to client-side templating. */
horizon.templates = {
template_ids: ["#modal_template", "#empty_row_template", "#alert_message_template", "#spinner-modal", "#project_user_template"],
template_ids: ["#modal_template", "#empty_row_template", "#alert_message_template", "#spinner-modal", "#membership_template"],
compiled_templates: {}
};

View File

@ -35,7 +35,7 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.templates.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.users.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.utils.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.projects.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.membership.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.networktopology.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.d3piechart.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.heattop.js' type='text/javascript' charset='utf-8'></script>

View File

@ -1,13 +1,13 @@
{% extends "horizon/client_side/template.html" %}
{% load horizon %}
{% block id %}project_user_template{% endblock %}
{% block id %}membership_template{% endblock %}
{% block template %}
{% jstemplate %}
<ul class="nav nav-pills btn-group">
<li class="member" data-user-id="[[user_id]]">
<span class="user_name">[[user_name]]</span>
<li class="member" data-[[step_slug]]-id="[[data_id]]">
<span class="display_name">[[display_name]]</span>
</li>
<li class="active"><a class="btn btn-primary" href="#add_remove">[[text]]</a></li>
<li class="dropdown role_options">

View File

@ -2,4 +2,4 @@
{% include "horizon/client_side/_table_row.html" %}
{% include "horizon/client_side/_alert_message.html" %}
{% include "horizon/client_side/_loading.html" %}
{% include "horizon/client_side/_project_user.html" %}
{% include "horizon/client_side/_membership.html" %}

View File

@ -2,34 +2,34 @@
<noscript><h3>{{ step }}</h3></noscript>
<div class="project_membership" data-show-roles="{{ step.show_roles|yesno }}">
<div class="membership {{ step.slug }}_membership" data-show-roles="{{ step.show_roles|yesno }}">
<div class="header">
<div class="help_text">{{ step.help_text }}</div>
<div class="left">
<div class="fake_table fake_table_header">
<span class="users_title">{{ step.available_list_title }}</span>
<input type="text" name="available_users_filter" id="available_users" class="filter" placeholder="Filter">
<div class="fake_table fake_table_header fake_{{ step.slug }}_table">
<span class="members_title">{{ step.available_list_title }}</span>
<input type="text" name="available_{{ step.slug }}_filter" id="available_{{ step.slug }}" class="filter {{ step.slug }}_filter" placeholder="Filter">
</div>
</div>
<div class="right">
<div class="fake_table fake_table_header">
<span class="users_title">{{ step.members_list_title }}</span>
<input type="text" name="project_members_filter" id="project_members" class="filter" placeholder="Filter">
<div class="fake_table fake_table_header fake_{{ step.slug }}_table">
<span class="members_title">{{ step.members_list_title }}</span>
<input type="text" name="{{ step.slug }}_members_filter" id="{{ step.slug }}_members" class="filter {{ step.slug }}_filter" placeholder="Filter">
</div>
</div>
</div>
<div class="left filterable">
<div class="fake_table" id="available_users">
<ul class="available_users"></ul>
<ul class="no_results" id="no_available_users"><li>{{ step.no_available_text }}</li></ul>
<div class="left filterable {{ step.slug }}_filterable">
<div class="fake_table fake_{{ step.slug }}_table" id="available_{{ step.slug }}">
<ul class="available_members available_{{ step.slug }}"></ul>
<ul class="no_results" id="no_available_{{ step.slug }}"><li>{{ step.no_available_text }}</li></ul>
</div>
</div>
<div class="right filterable">
<div class="fake_table" id="project_members">
<ul class="project_members"></ul>
<ul class="no_results" id="no_project_members"><li>{{ step.no_members_text }}</li></ul>
<div class="right filterable {{ step.slug }}_filterable">
<div class="fake_table fake_{{ step.slug }}_table" id="{{ step.slug }}_members">
<ul class="members {{ step.slug }}_members"></ul>
<ul class="no_results" id="no_{{ step.slug }}_members"><li>{{ step.no_members_text }}</li></ul>
</div>
</div>
</div>
@ -41,10 +41,10 @@
<script>
if (typeof $ !== 'undefined') {
horizon.projects.workflow_init($(".workflow"));
horizon.membership.workflow_init($(".workflow"), "{{ step.slug }}", "{{ step.get_id }}");
} else {
addHorizonLoadEvent(function() {
horizon.projects.workflow_init($(".workflow"));
horizon.membership.workflow_init($(".workflow"), "{{ step.slug }}", "{{ step.get_id }}");
});
}
</script>

View File

@ -1,10 +1,12 @@
from horizon.workflows.base import Action
from horizon.workflows.base import MembershipAction
from horizon.workflows.base import Step
from horizon.workflows.base import UpdateMembersStep
from horizon.workflows.base import Workflow
from horizon.workflows.views import WorkflowView
assert Action
assert MembershipAction
assert Step
assert UpdateMembersStep
assert Workflow

View File

@ -144,7 +144,7 @@ class Action(forms.Form):
return force_unicode(self.name)
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self.slug)
return "<%s: %s>" % (self.__class__.__name_get_step_, self.slug)
def _populate_choices(self, request, context):
for field_name, bound_field in self.fields.items():
@ -181,6 +181,20 @@ class Action(forms.Form):
return None
class MembershipAction(Action):
"""
An action that allows a user to add/remove members from a group.
Extend the Action class with additional helper method for membership
management.
"""
def get_default_role_field_name(self):
return "default_" + self.slug + "_role"
def get_member_field_name(self, role_id):
return self.slug + "_role_" + role_id
class Step(object):
"""
A step is a wrapper around an action which defines it's context in a
@ -470,6 +484,12 @@ class UpdateMembersStep(Step):
no_available_text = _("None available.")
no_members_text = _("No members.")
def get_member_field_name(self, role_id):
if issubclass(self.action_class, MembershipAction):
return self.action.get_member_field_name(role_id)
else:
return self.slug + "_role_" + role_id
class Workflow(html.HTMLElement):
"""

View File

@ -1,33 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from horizon import forms
from openstack_dashboard.dashboards.admin.users.forms import CreateUserForm
class CreateUser(CreateUserForm):
role_id = forms.ChoiceField(widget=forms.HiddenInput())
project = forms.CharField(widget=forms.HiddenInput())
def __init__(self, request, *args, **kwargs):
super(CreateUser, self).__init__(request, *args, **kwargs)
project = self.request.path.split("/")[-1]
self.fields['project'].initial = project

View File

@ -4,13 +4,10 @@ from django.core.urlresolvers import reverse
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.users.tables import UsersTable
LOG = logging.getLogger(__name__)
@ -108,61 +105,3 @@ class TenantsTable(tables.DataTable):
table_actions = (TenantFilterAction, CreateProject,
DeleteTenantsAction)
pagination_param = "tenant_marker"
class RemoveUserAction(tables.BatchAction):
name = "remove_user"
action_present = _("Remove")
action_past = _("Removed")
data_type_singular = _("User")
data_type_plural = _("Users")
classes = ('btn-danger',)
def action(self, request, user_id):
tenant_id = self.table.kwargs['tenant_id']
api.keystone.remove_tenant_user(request, tenant_id, user_id)
class ProjectUserRolesColumn(tables.Column):
def get_raw_data(self, user):
request = self.table.request
try:
roles = api.keystone.roles_for_user(request,
user.id,
self.table.kwargs["tenant_id"])
except:
roles = []
exceptions.handle(request,
_("Unable to retrieve role information."))
return ", ".join([role.name for role in roles])
class TenantUsersTable(UsersTable):
roles = ProjectUserRolesColumn("roles", verbose_name=_("Roles"))
class Meta:
name = "tenant_users"
verbose_name = _("Users For Project")
table_actions = (RemoveUserAction,)
row_actions = (RemoveUserAction,)
columns = ("name", "email", "id", "roles", "enabled")
class AddUserAction(tables.LinkAction):
name = "add_user"
verbose_name = _("Add To Project")
url = "horizon:admin:projects:add_user"
classes = ('ajax-modal',)
def get_link_url(self, user):
tenant_id = self.table.kwargs['tenant_id']
return reverse(self.url, args=(tenant_id, user.id))
class AddUsersTable(UsersTable):
class Meta:
name = "add_users"
verbose_name = _("Add New Users")
table_actions = ()
row_actions = (AddUserAction,)
columns = ("name", "email", "id", "enabled")

View File

@ -1,26 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}add_user_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:projects:add_user' tenant_id user_id %}{% endblock %}
{% block modal_id %}add_user_modal{% endblock %}
{% block modal-header %}{% trans "Add User To Project" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "Select the user role for the project." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Add" %}" />
<a href="{% url 'horizon:admin:projects:users' tenant_id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}create_tenant_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:projects:create' %}{% endblock %}
{% block modal_id %}create_tenant_modal{% endblock %}
{% block modal-header %}{% trans "Create Project" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "From here you can create a new project to organize users." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Project" %}" />
<a href="{% url 'horizon:admin:projects:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}create_user_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:projects:create_user' tenant_id %}{% endblock %}
{% block modal-header %}{% blocktrans %}Create User for project '{{ tenant_name }}'.{% endblocktrans %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "From here you can create a new user to add to this project." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create User" %}" />
<a href="{% url 'horizon:admin:projects:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,25 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}quota_update_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:projects:quotas' tenant.id %}{% endblock %}
{% block modal-header %}{% trans "Update Quota" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% blocktrans with tenant_id=tenant.id %}From here you can edit quotas (max limits) for the project {{ tenant.name }}.{% endblocktrans %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Quota" %}" />
<a href="{% url 'horizon:admin:projects:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:admin:projects:update' tenant.id %}{% endblock %}
{% block modal_id %}update_tenant_modal{% endblock %}
{% block modal-header %}{% trans "Update Project" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "From here you can edit a project." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Project" %}" />
<a href="{% url 'horizon:admin:projects:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,39 +0,0 @@
{% load i18n %}
<noscript><h3>{{ step }}</h3></noscript>
<div class="project_membership">
<div class="header">
<div class="help_text">{% trans "From here you can add and remove members to this project from the list of all available users." %}</div>
<div class="left">
<div class="fake_table fake_table_header">
<span class="users_title">{% trans "All Users" %}</span>
<input type="text" name="available_users_filter" id="available_users" class="filter" value="Filter">
</div>
</div>
<div class="right">
<div class="fake_table fake_table_header">
<span class="users_title">{% trans "Project Members" %}</span>
<input type="text" name="project_members_filter" id="project_members" class="filter" value="Filter">
</div>
</div>
</div>
<div class="left filterable">
<div class="fake_table" id="available_users">
<ul class="available_users"></ul>
<ul class="no_results" id="no_available_users"><li>{% trans "No users found." %}</li></ul>
</div>
</div>
<div class="right filterable">
<div class="fake_table" id="project_members">
<ul class="project_members"></ul>
<ul class="no_results" id="no_project_members"><li>{% trans "No users found." %}</li></ul>
</div>
</div>
</div>
<div class="hide">
{% include "horizon/common/_form_fields.html" %}
</div>

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Add User To Project" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Add User To Project") %}
{% endblock page_header %}
{% block main %}
{% include 'admin/projects/_add_user.html' %}
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Add New User" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Add New User") %}
{% endblock page_header %}
{% block main %}
{% include 'horizon/common/_create_user.html' %}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Modify Project Quotas" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Update Project") %}
{% endblock page_header %}
{% block main %}
{% include 'admin/projects/_quotas.html' with form=form %}
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Project Users" %}{% endblock %}
{% block page_header %}
<div class='page-header'>
<h2>{% trans "Users for Project" %}: <span>{{ tenant.name }}</span></h2>
</div>
{% endblock %}
{% block main %}
<div id="tenant_users_table">
{{ tenant_users_table.render }}
</div>
<div id="add_users_table">
{{ add_users_table.render }}
</div>
{% endblock %}

View File

@ -30,10 +30,14 @@ from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.admin.projects.workflows \
import CreateProject
from openstack_dashboard.dashboards.admin.projects.workflows \
import PROJECT_USER_MEMBER_SLUG
from openstack_dashboard.dashboards.admin.projects.workflows \
import UpdateProject
INDEX_URL = reverse('horizon:admin:projects:index')
USER_ROLE_PREFIX = PROJECT_USER_MEMBER_SLUG + "_role_"
@test.create_stubs({api.keystone: ('tenant_list',)})
@ -176,10 +180,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
# contribute
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
# handle
project_details = self._get_project_info(project)
@ -188,12 +190,10 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
.AndReturn(project)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
workflow_data = {}
for role in roles:
if "role_" + role.id in workflow_data:
ulist = workflow_data["role_" + role.id]
if USER_ROLE_PREFIX + role.id in workflow_data:
ulist = workflow_data[USER_ROLE_PREFIX + role.id]
for user_id in ulist:
api.keystone.add_tenant_user_role(IsA(http.HttpRequest),
project=self.tenant.id,
@ -245,7 +245,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
self.mox.ReplayAll()
@ -281,10 +282,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
# contribute
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
# handle
project_details = self._get_project_info(project)
@ -330,10 +329,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
# contribute
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
# handle
project_details = self._get_project_info(project)
@ -342,12 +339,10 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
.AndReturn(project)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
workflow_data = {}
for role in roles:
if "role_" + role.id in workflow_data:
ulist = workflow_data["role_" + role.id]
if USER_ROLE_PREFIX + role.id in workflow_data:
ulist = workflow_data[USER_ROLE_PREFIX + role.id]
for user_id in ulist:
api.keystone.add_tenant_user_role(IsA(http.HttpRequest),
project=self.tenant.id,
@ -400,10 +395,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
# contribute
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
# handle
project_details = self._get_project_info(project)
@ -412,12 +405,10 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
.AndReturn(project)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
workflow_data = {}
for role in roles:
if "role_" + role.id in workflow_data:
ulist = workflow_data["role_" + role.id]
if USER_ROLE_PREFIX + role.id in workflow_data:
ulist = workflow_data[USER_ROLE_PREFIX + role.id]
for user_id in ulist:
api.keystone.add_tenant_user_role(IsA(http.HttpRequest),
project=self.tenant.id,
@ -474,10 +465,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
# contribute
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
self.mox.ReplayAll()
@ -546,7 +535,8 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
for user in users:
api.keystone.roles_for_user(IsA(http.HttpRequest),
@ -614,7 +604,8 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
workflow_data = {}
for user in users:
@ -622,8 +613,8 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
user.id,
self.tenant.id).AndReturn(roles)
workflow_data["role_1"] = ['3'] # admin role
workflow_data["role_2"] = ['2'] # member role
workflow_data[USER_ROLE_PREFIX + "1"] = ['3'] # admin role
workflow_data[USER_ROLE_PREFIX + "2"] = ['2'] # member role
# update some fields
project._info["name"] = "updated name"
@ -636,16 +627,12 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
updated_quota = self._get_quota_info(quota)
# contribute
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
# handle
api.keystone.tenant_update(IsA(http.HttpRequest),
project.id,
**updated_project) \
.AndReturn(project)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.user_list(IsA(http.HttpRequest),
project=self.tenant.id).AndReturn(proj_users)
@ -763,7 +750,8 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
workflow_data = {}
for user in users:
@ -772,7 +760,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
self.tenant.id).AndReturn(roles)
role_ids = [role.id for role in roles]
if role_ids:
workflow_data.setdefault("role_" + role_ids[0], []) \
workflow_data.setdefault(USER_ROLE_PREFIX + role_ids[0], []) \
.append(user.id)
# update some fields
@ -786,9 +774,6 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
updated_quota = self._get_quota_info(quota)
# contribute
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
# handle
api.keystone.tenant_update(IsA(http.HttpRequest),
project.id,
@ -848,7 +833,8 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
workflow_data = {}
@ -857,8 +843,8 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
user.id,
self.tenant.id).AndReturn(roles)
workflow_data["role_1"] = ['1', '3'] # admin role
workflow_data["role_2"] = ['1', '2', '3'] # member role
workflow_data[USER_ROLE_PREFIX + "1"] = ['1', '3'] # admin role
workflow_data[USER_ROLE_PREFIX + "2"] = ['1', '2', '3'] # member role
# update some fields
project._info["name"] = "updated name"
@ -871,17 +857,12 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
updated_quota = self._get_quota_info(quota)
# contribute
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
# handle
# handle
api.keystone.tenant_update(IsA(http.HttpRequest),
project.id,
**updated_project) \
.AndReturn(project)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.user_list(IsA(http.HttpRequest),
project=self.tenant.id).AndReturn(proj_users)
@ -962,18 +943,20 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
.AndReturn(quota)
api.keystone.get_default_role(IsA(http.HttpRequest)) \
.AndReturn(default_role)
.MultipleTimes().AndReturn(default_role)
api.keystone.user_list(IsA(http.HttpRequest), domain=domain_id) \
.AndReturn(users)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.role_list(IsA(http.HttpRequest)) \
.MultipleTimes().AndReturn(roles)
workflow_data = {}
for user in users:
api.keystone.roles_for_user(IsA(http.HttpRequest),
user.id,
self.tenant.id).AndReturn(roles)
workflow_data["role_1"] = ['1', '3'] # admin role
workflow_data["role_2"] = ['1', '2', '3'] # member role
workflow_data[USER_ROLE_PREFIX + "1"] = ['1', '3'] # admin role
workflow_data[USER_ROLE_PREFIX + "2"] = ['1', '2', '3'] # member role
# update some fields
project._info["name"] = "updated name"
@ -986,16 +969,12 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
updated_quota = self._get_quota_info(quota)
# contribute
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
# handle
api.keystone.tenant_update(IsA(http.HttpRequest),
project.id,
**updated_project) \
.AndReturn(project)
api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
api.keystone.user_list(IsA(http.HttpRequest),
project=self.tenant.id).AndReturn(proj_users)
@ -1047,7 +1026,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
quota = self.quotas.first()
api.keystone.get_default_role(IsA(http.HttpRequest)) \
.AndReturn(None) # Default role doesn't exist
.MultipleTimes().AndReturn(None) # Default role doesn't exist
api.keystone.tenant_get(IsA(http.HttpRequest), self.tenant.id,
admin=True) \
.AndReturn(project)

View File

@ -23,7 +23,6 @@ from django.conf.urls.defaults import url
from openstack_dashboard.dashboards.admin.projects.views \
import CreateProjectView
from openstack_dashboard.dashboards.admin.projects.views import CreateUserView
from openstack_dashboard.dashboards.admin.projects.views import IndexView
from openstack_dashboard.dashboards.admin.projects.views \
import ProjectUsageView
@ -38,6 +37,4 @@ urlpatterns = patterns('',
UpdateProjectView.as_view(), name='update'),
url(r'^(?P<tenant_id>[^/]+)/usage/$',
ProjectUsageView.as_view(), name='usage'),
url(r'^(?P<tenant_id>[^/]+)/create_user/$',
CreateUserView.as_view(), name='create_user'),
)

View File

@ -21,7 +21,6 @@
import logging
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
@ -29,15 +28,10 @@ from horizon import tables
from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.users.views import CreateView
from openstack_dashboard import usage
from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.admin.projects.forms import CreateUser
from openstack_dashboard.dashboards.admin.projects.tables import AddUsersTable
from openstack_dashboard.dashboards.admin.projects.tables import TenantsTable
from openstack_dashboard.dashboards.admin.projects.tables \
import TenantUsersTable
from openstack_dashboard.dashboards.admin.projects.workflows \
import CreateProject
from openstack_dashboard.dashboards.admin.projects.workflows \
@ -97,45 +91,6 @@ class IndexView(tables.DataTableView):
return tenants
class UsersView(tables.MultiTableView):
table_classes = (TenantUsersTable, AddUsersTable)
template_name = 'admin/projects/users.html'
def _get_shared_data(self, *args, **kwargs):
tenant_id = self.kwargs["tenant_id"]
if not hasattr(self, "_shared_data"):
domain_context = self.request.session.get('domain_context', None)
try:
tenant = api.keystone.tenant_get(self.request,
tenant_id,
admin=True)
all_users = api.keystone.user_list(self.request,
domain=domain_context)
tenant_users = api.keystone.user_list(self.request, tenant_id)
self._shared_data = {'tenant': tenant,
'all_users': all_users,
'tenant_users': tenant_users}
except:
exceptions.handle(self.request,
_("Unable to retrieve users."),
redirect=reverse(INDEX_URL))
return self._shared_data
def get_tenant_users_data(self):
return self._get_shared_data()["tenant_users"]
def get_add_users_data(self):
tenant_users = self._get_shared_data()["tenant_users"]
all_users = self._get_shared_data()["all_users"]
tenant_user_ids = [user.id for user in tenant_users]
return filter(lambda u: u.id not in tenant_user_ids, all_users)
def get_context_data(self, **kwargs):
context = super(UsersView, self).get_context_data(**kwargs)
context['tenant'] = self._get_shared_data()["tenant"]
return context
class ProjectUsageView(usage.UsageView):
table_class = usage.ProjectUsageTable
usage_class = usage.ProjectUsage
@ -191,23 +146,3 @@ class UpdateProjectView(workflows.WorkflowView):
_('Unable to retrieve project details.'),
redirect=reverse(INDEX_URL))
return initial
class CreateUserView(CreateView):
form_class = CreateUser
template_name = "admin/projects/create_user.html"
success_url = reverse_lazy('horizon:admin:projects:index')
def get_initial(self):
default_role = api.keystone.get_default_role(self.request)
return {'role_id': getattr(default_role, "id", None),
'project': self.kwargs['tenant_id']}
def get_context_data(self, **kwargs):
context = super(CreateUserView, self).get_context_data(**kwargs)
context['tenant_id'] = self.kwargs['tenant_id']
context['tenant_name'] = api.keystone.tenant_get(
self.request,
self.kwargs['tenant_id'],
admin=True).name
return context

View File

@ -39,6 +39,7 @@ from openstack_dashboard.usage.quotas import QUOTA_FIELDS
INDEX_URL = "horizon:admin:projects:index"
ADD_USER_URL = "horizon:admin:projects:create_user"
PROJECT_USER_MEMBER_SLUG = "update_members"
class UpdateProjectQuotaAction(workflows.Action):
@ -108,9 +109,7 @@ class CreateProjectInfo(workflows.Step):
"enabled")
class UpdateProjectMembersAction(workflows.Action):
default_role = forms.CharField(required=False)
class UpdateProjectMembersAction(workflows.MembershipAction):
def __init__(self, request, *args, **kwargs):
super(UpdateProjectMembersAction, self).__init__(request,
*args,
@ -134,7 +133,9 @@ class UpdateProjectMembersAction(workflows.Action):
exceptions.handle(self.request,
err_msg,
redirect=reverse(INDEX_URL))
self.fields['default_role'].initial = default_role.id
default_role_name = self.get_default_role_field_name()
self.fields[default_role_name] = forms.CharField(required=False)
self.fields[default_role_name].initial = default_role.id
# Get list of available users
all_users = []
@ -155,7 +156,7 @@ class UpdateProjectMembersAction(workflows.Action):
err_msg,
redirect=reverse(INDEX_URL))
for role in role_list:
field_name = "role_" + role.id
field_name = self.get_member_field_name(role.id)
label = _(role.name)
self.fields[field_name] = forms.MultipleChoiceField(required=False,
label=label)
@ -174,11 +175,12 @@ class UpdateProjectMembersAction(workflows.Action):
err_msg,
redirect=reverse(INDEX_URL))
for role in roles:
self.fields["role_" + role.id].initial.append(user.id)
field_name = self.get_member_field_name(role.id)
self.fields[field_name].initial.append(user.id)
class Meta:
name = _("Project Members")
slug = "update_members"
slug = PROJECT_USER_MEMBER_SLUG
class UpdateProjectMembers(workflows.UpdateMembersStep):
@ -198,7 +200,7 @@ class UpdateProjectMembers(workflows.UpdateMembersStep):
post = self.workflow.request.POST
for role in roles:
field = "role_" + role.id
field = self.get_member_field_name(role.id)
context[field] = post.getlist(field)
return context
@ -214,6 +216,14 @@ class CreateProject(workflows.Workflow):
UpdateProjectMembers,
UpdateProjectQuota)
def __init__(self, request=None, context_seed=None, entry_point=None,
*args, **kwargs):
super(CreateProject, self).__init__(request=request,
context_seed=context_seed,
entry_point=entry_point,
*args,
**kwargs)
def format_status_message(self, message):
return message % self.context.get('name', 'unknown project')
@ -237,14 +247,16 @@ class CreateProject(workflows.Workflow):
users_to_add = 0
try:
available_roles = api.keystone.role_list(request)
member_step = self.get_step(PROJECT_USER_MEMBER_SLUG)
# count how many users are to be added
for role in available_roles:
role_list = data["role_" + role.id]
field_name = member_step.get_member_field_name(role.id)
role_list = data[field_name]
users_to_add += len(role_list)
# add new users to project
for role in available_roles:
role_list = data["role_" + role.id]
field_name = member_step.get_member_field_name(role.id)
role_list = data[field_name]
users_added = 0
for user in role_list:
api.keystone.add_tenant_user_role(request,
@ -255,8 +267,8 @@ class CreateProject(workflows.Workflow):
users_to_add -= users_added
except:
exceptions.handle(request, _('Failed to add %s project members '
'and set project quotas.'
% users_to_add))
'and set project quotas.')
% users_to_add)
# Update the project quota.
nova_data = dict([(key, data[key]) for key in NOVA_QUOTA_FIELDS])
@ -302,6 +314,14 @@ class UpdateProject(workflows.Workflow):
UpdateProjectMembers,
UpdateProjectQuota)
def __init__(self, request=None, context_seed=None, entry_point=None,
*args, **kwargs):
super(UpdateProject, self).__init__(request=request,
context_seed=context_seed,
entry_point=entry_point,
*args,
**kwargs)
def format_status_message(self, message):
return message % self.context.get('name', 'unknown project')
@ -322,12 +342,14 @@ class UpdateProject(workflows.Workflow):
exceptions.handle(request, ignore=True)
return False
# Get our role options
available_roles = api.keystone.role_list(request)
# update project members
users_to_modify = 0
# Project-user member step
member_step = self.get_step(PROJECT_USER_MEMBER_SLUG)
try:
# Get our role options
available_roles = api.keystone.role_list(request)
# Get the users currently associated with this project so we
# can diff against it.
project_members = api.keystone.user_list(request,
@ -341,9 +363,11 @@ class UpdateProject(workflows.Workflow):
user.id,
project_id)
current_role_ids = [role.id for role in current_roles]
for role in available_roles:
field_name = member_step.get_member_field_name(role.id)
# Check if the user is in the list of users with this role.
if user.id in data["role_" + role.id]:
if user.id in data[field_name]:
# Add it if necessary
if role.id not in current_role_ids:
# user role has changed
@ -389,11 +413,13 @@ class UpdateProject(workflows.Workflow):
# Grant new roles on the project.
for role in available_roles:
field_name = member_step.get_member_field_name(role.id)
# Count how many users may be added for exception handling.
users_to_modify += len(data["role_" + role.id])
users_to_modify += len(data[field_name])
for role in available_roles:
users_added = 0
for user_id in data["role_" + role.id]:
field_name = member_step.get_member_field_name(role.id)
for user_id in data[field_name]:
if not filter(lambda x: user_id == x.id, project_members):
api.keystone.add_tenant_user_role(request,
project=project_id,
@ -403,8 +429,8 @@ class UpdateProject(workflows.Workflow):
users_to_modify -= users_added
except:
exceptions.handle(request, _('Failed to modify %s project members '
'and update project quotas.'
% users_to_modify))
'and update project quotas.')
% users_to_modify)
return True
# update the project quota

View File

@ -39,11 +39,14 @@ from openstack_dashboard.dashboards.project.instances.tabs \
import InstanceDetailTabs
from openstack_dashboard.dashboards.project.instances.workflows \
import LaunchInstance
from openstack_dashboard.dashboards.project.instances.workflows \
import update_instance
from novaclient.v1_1.servers import REBOOT_HARD
from novaclient.v1_1.servers import REBOOT_SOFT
INDEX_URL = reverse('horizon:project:instances:index')
SEC_GROUP_ROLE_PREFIX = update_instance.INSTANCE_SEC_GROUP_SLUG + "_role_"
class InstanceTests(test.TestCase):
@ -723,9 +726,11 @@ class InstanceTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
def _instance_update_post(self, server_id, server_name, secgroups):
default_role_field_name = 'default_' + \
update_instance.INSTANCE_SEC_GROUP_SLUG + '_role'
formData = {'name': server_name,
'default_role': 'member',
'role_member': secgroups}
default_role_field_name: 'member',
SEC_GROUP_ROLE_PREFIX + 'member': secgroups}
url = reverse('horizon:project:instances:update',
args=[server_id])
return self.client.post(url, formData)

View File

@ -28,15 +28,12 @@ from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.utils.filters import get_int_or_uuid
INDEX_URL = "horizon:projects:instances:index"
ADD_USER_URL = "horizon:projects:instances:create_user"
INSTANCE_SEC_GROUP_SLUG = "update_security_groups"
class UpdateInstanceSecurityGroupsAction(workflows.Action):
default_role = forms.CharField(required=False)
role_member = forms.MultipleChoiceField(required=False)
class UpdateInstanceSecurityGroupsAction(workflows.MembershipAction):
def __init__(self, request, *args, **kwargs):
super(UpdateInstanceSecurityGroupsAction, self).__init__(request,
*args,
@ -46,7 +43,9 @@ class UpdateInstanceSecurityGroupsAction(workflows.Action):
context = args[0]
instance_id = context.get('instance_id', '')
self.fields['default_role'].initial = 'member'
default_role_name = self.get_default_role_field_name()
self.fields[default_role_name] = forms.CharField(required=False)
self.fields[default_role_name].initial = 'member'
# Get list of available security groups
all_groups = []
@ -62,9 +61,11 @@ class UpdateInstanceSecurityGroupsAction(workflows.Action):
instance_id)
except Exception:
exceptions.handle(request, err_msg)
self.fields['role_member'].choices = groups_list
self.fields['role_member'].initial = [group.id
for group in instance_groups]
field_name = self.get_member_field_name('member')
self.fields[field_name] = forms.MultipleChoiceField(required=False)
self.fields[field_name].choices = groups_list
self.fields[field_name].initial = [group.id
for group in instance_groups]
def handle(self, request, data):
instance_id = data['instance_id']
@ -79,7 +80,7 @@ class UpdateInstanceSecurityGroupsAction(workflows.Action):
class Meta:
name = _("Security Groups")
slug = "update_security_groups"
slug = INSTANCE_SEC_GROUP_SLUG
class UpdateInstanceSecurityGroups(workflows.UpdateMembersStep):
@ -97,7 +98,8 @@ class UpdateInstanceSecurityGroups(workflows.UpdateMembersStep):
def contribute(self, data, context):
request = self.workflow.request
if data:
context["wanted_groups"] = request.POST.getlist("role_member")
field_name = self.get_member_field_name('member')
context["wanted_groups"] = request.POST.getlist(field_name)
return context

View File

@ -1367,8 +1367,8 @@ label.log-length {
margin-top: -60px;
}
/* Project Membership UI */
.project_membership {
/* Membership widget UI */
.membership {
min-height: 200px;
/* Buttons */
@ -1391,7 +1391,7 @@ label.log-length {
margin-left: 15px;
margin-bottom: 15px;
}
.users_title {
.members_title {
color: #555;
font-weight: bold;
padding-left: 10px;
@ -1435,16 +1435,16 @@ label.log-length {
}
}
/* User lists */
/* Member lists */
.member {
padding: 10px;
text-align: left;
}
.project_members {
.members {
margin-left: -20px;
}
.project_members ul.btn-group,
.available_users ul.btn-group {
.members ul.btn-group,
.available_members ul.btn-group {
width: 308px;
}
.dark_stripe {
@ -1505,14 +1505,14 @@ label.log-length {
}
}
/* Inline user creation */
.add_user_btn {
/* Inline member creation */
.add_member_btn {
display: inline;
}
#add_user {
#add_member {
clear: both;
}
.add_user {
.add_member {
float: right;
margin-top: 10px;
margin-right: 15px;